feat(tags): legacy-tag purge also catches source:* general tags

Operator-confirmed 2026-05-28: the BlenderKnight:* tags are `archive`
kind (caught by the kind purge), but the `source:patreon`-style tags
are IR's old `source` kind that fell back to `general` during migration
(FC's enum has no `source` kind) — so they can't be matched by kind.

Broadened the purge to a two-rule match and renamed it for accuracy
(all dev-only, unreleased):
- cleanup_service.purge_tags_by_kind → purge_legacy_tags. Predicate is
  now `kind IN (archive, post, artist) OR name LIKE 'source:%'`
  (LEGACY_NAME_PREFIXES). Preview classifies each row into by_kind OR
  by_prefix (source:* counts once under the prefix bucket regardless of
  its general kind).
- endpoint /tags/purge-retired-kinds → /tags/purge-legacy. dry-run
  returns by_kind + by_prefix + count + sample.
- store purgeRetiredKindTags → purgeLegacyTags.
- Tag Maintenance card copy + breakdown updated to show both buckets;
  button reads "Preview/Delete legacy tags".

Tests updated + extended: dry-run reports by_kind {archive,post,artist}
AND by_prefix {source:*}, plain general/character tags survive; commit
deletes both the kind-matched and source:*-matched rows and leaves the
rest.
This commit is contained in:
2026-05-28 01:20:41 -04:00
parent e1fc65bd1b
commit bf8eb4468f
5 changed files with 104 additions and 66 deletions
@@ -48,12 +48,14 @@
<v-divider class="my-5" />
<p class="fc-muted text-body-2 mb-4">
Purge tags with retired/system kinds FC no longer uses
(<code>archive</code>, <code>post</code>, <code>artist</code>)
IR-migration leftovers like
<code>BlenderKnight:Hannah_BJ_Loops</code>. Provenance and
artists are their own systems now, so these tag rows are pure
noise. Removes them from every image.
Purge legacy IR-migration tags FC no longer uses: retired/system
kinds (<code>archive</code>, <code>post</code>,
<code>artist</code> e.g.
<code>BlenderKnight:Hannah_BJ_Loops</code>) plus
<code>source:*</code> tags (ImageRepo's old <code>source</code>
kind, which migrated to <code>general</code>). Provenance and
artists are their own systems now, so these are pure noise.
Removes them from every image.
</p>
<v-btn
@@ -62,14 +64,17 @@
:loading="loadingKindPreview"
class="mb-3"
@click="onKindPreview"
>Preview retired-kind tags</v-btn>
>Preview legacy tags</v-btn>
<div v-if="kindPreview">
<p class="text-body-2 mb-2">
<strong>{{ kindPreview.count }}</strong> tag(s) of retired kinds.
<strong>{{ kindPreview.count }}</strong> legacy tag(s).
<span v-for="(n, k) in kindPreview.by_kind" :key="k" class="fc-muted">
{{ k }}: {{ n }}&nbsp;&nbsp;
</span>
<span v-for="(n, p) in kindPreview.by_prefix" :key="p" class="fc-muted">
{{ p }}: {{ n }}&nbsp;&nbsp;
</span>
</p>
<div v-if="kindPreview.sample_names?.length" class="fc-name-grid mb-3">
<span v-for="n in kindPreview.sample_names" :key="n" class="fc-name">
@@ -82,7 +87,7 @@
:disabled="!kindPreview.count"
:loading="kindCommitting"
@click="onKindCommit"
>Delete {{ kindPreview.count }} retired-kind tag(s)</v-btn>
>Delete {{ kindPreview.count }} legacy tag(s)</v-btn>
</div>
</v-card-text>
</v-card>
@@ -123,7 +128,7 @@ async function onCommit() {
async function onKindPreview() {
loadingKindPreview.value = true
try {
kindPreview.value = await store.purgeRetiredKindTags({ dryRun: true })
kindPreview.value = await store.purgeLegacyTags({ dryRun: true })
} finally {
loadingKindPreview.value = false
}
@@ -132,8 +137,8 @@ async function onKindPreview() {
async function onKindCommit() {
kindCommitting.value = true
try {
await store.purgeRetiredKindTags({ dryRun: false })
kindPreview.value = { count: 0, by_kind: {}, sample_names: [] }
await store.purgeLegacyTags({ dryRun: false })
kindPreview.value = { count: 0, by_kind: {}, by_prefix: {}, sample_names: [] }
} finally {
kindCommitting.value = false
}