diff --git a/backend/app/services/gallery_service.py b/backend/app/services/gallery_service.py
index 4cde351..94e949f 100644
--- a/backend/app/services/gallery_service.py
+++ b/backend/app/services/gallery_service.py
@@ -22,7 +22,15 @@ from sqlalchemy import Select, and_, distinct, exists, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import aliased
-from ..models import Artist, ImageProvenance, ImageRecord, Post, Source, Tag
+from ..models import (
+ Artist,
+ ImageProvenance,
+ ImageRecord,
+ Post,
+ Source,
+ Tag,
+ TagPositiveConfirmation,
+)
from ..models.tag import PRESENTATION_SYSTEM_TAGS, image_tag
from .pagination import decode_cursor, encode_cursor
from .tag_query import (
@@ -731,8 +739,14 @@ class GalleryService:
# Self-join Tag to resolve a character's fandom NAME (not just id) so the
# modal chip can label it without an N+1 (shared tag_query helpers).
fandom_alias = fandom_join_alias()
+ # source drives the auto-applied badge; confirmed = operator affirmed the
+ # tag (positive + retraction-shielded, milestone 139).
+ confirmed = exists().where(
+ TagPositiveConfirmation.image_record_id == image_id,
+ TagPositiveConfirmation.tag_id == Tag.id,
+ ).label("confirmed")
tag_stmt = (
- select(*tag_columns(fandom_alias))
+ select(*tag_columns(fandom_alias), image_tag.c.source, confirmed)
.select_from(
Tag.__table__
.join(image_tag, image_tag.c.tag_id == Tag.id)
diff --git a/backend/app/services/tag_query.py b/backend/app/services/tag_query.py
index 13186dc..a60340e 100644
--- a/backend/app/services/tag_query.py
+++ b/backend/app/services/tag_query.py
@@ -91,4 +91,11 @@ def serialize_tag(row) -> dict:
"fandom_id": row.fandom_id,
"fandom_name": row.fandom_name,
"is_system": bool(getattr(row, "is_system", False)),
+ # Applied-tag context: only the image-scoped selects (list_for_image /
+ # get_image_with_tags) provide these; autocomplete / directory don't →
+ # default. `source` drives the auto-applied badge; `confirmed` = the
+ # operator affirmed the tag (a training positive, shielded from the
+ # retraction sweep — milestone 139).
+ "source": getattr(row, "source", None),
+ "confirmed": bool(getattr(row, "confirmed", False)),
}
diff --git a/backend/app/services/tag_service.py b/backend/app/services/tag_service.py
index 98e1d9c..b6fe1d6 100644
--- a/backend/app/services/tag_service.py
+++ b/backend/app/services/tag_service.py
@@ -9,7 +9,14 @@ from sqlalchemy import and_, case, exists, func, select, text, update
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession
-from ..models import HeadMetric, Tag, TagHead, TagKind, image_tag
+from ..models import (
+ HeadMetric,
+ Tag,
+ TagHead,
+ TagKind,
+ TagPositiveConfirmation,
+ image_tag,
+)
from .db_helpers import get_or_create
from .tag_query import fandom_join_alias, tag_columns
@@ -288,8 +295,14 @@ class TagService:
character chip with its fandom without an N+1 (mirrors the
autocomplete/directory resolution)."""
fandom_alias = fandom_join_alias()
+ # source drives the auto-applied badge; confirmed = operator affirmed the
+ # tag (positive + retraction-shielded, milestone 139).
+ confirmed = exists().where(
+ TagPositiveConfirmation.image_record_id == image_id,
+ TagPositiveConfirmation.tag_id == Tag.id,
+ ).label("confirmed")
stmt = (
- select(*tag_columns(fandom_alias))
+ select(*tag_columns(fandom_alias), image_tag.c.source, confirmed)
.select_from(
Tag.__table__
.join(image_tag, image_tag.c.tag_id == Tag.id)
diff --git a/frontend/src/components/modal/TagChip.vue b/frontend/src/components/modal/TagChip.vue
index 6437255..7cf2f59 100644
--- a/frontend/src/components/modal/TagChip.vue
+++ b/frontend/src/components/modal/TagChip.vue
@@ -16,8 +16,20 @@
>mdi-shield-outline→ {{ fandomLabel }}
+ >→ {{ fandomLabel }}auto
+
+