fix(tags): time-box + self-resume the tag standardization (stop the 40-min timeout)
CI / lint (push) Failing after 2s
CI / backend-lint-and-test (push) Failing after 9s
CI / frontend-build (push) Successful in 18s
CI / integration (push) Failing after 2m17s

normalize_tags_task timed out at the 40-min hard limit on a large back-catalog
(the first run recases the whole booru vocabulary) — operator-flagged, and it
monopolized the concurrency-1 maintenance queue while doing so.

normalize_existing_tags now takes time_budget_seconds: the live run stops
cleanly at the budget and reports {partial, remaining}. The task runs 600s
chunks and re-enqueues itself until nothing remains (idempotent — commits per
group, so the next chunk skips already-canonical groups). Short chunks let the
recovery sweep and other maintenance tasks interleave instead of being blocked
for 40 minutes.

Frontend: the Standardize button is now fire-and-forget ("Queued — runs in the
background; re-run Preview to confirm") instead of poll-until-done, which would
have falsely reported "complete" after the first chunk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 23:57:06 -04:00
parent 1819caaf5b
commit d9d502a60d
4 changed files with 90 additions and 21 deletions
@@ -185,13 +185,9 @@
:loading="normCommitting"
@click="onNormCommit"
>Standardize {{ normPreview.total_changes }} tag group(s)</v-btn>
<span
v-if="normResult"
class="ml-3 text-caption"
:class="normResult === 'ok' ? 'text-success' : 'text-warning'"
>
<template v-if="normResult === 'ok'">Standardization complete ✓</template>
<template v-else>Finished with status: {{ normResult }}</template>
<span v-if="normResult === 'queued'" class="ml-3 text-caption text-success">
Queued ✓ — runs in the background (you can leave this page). It
processes in chunks, so re-run “Preview” later to confirm it's all done.
</span>
</div>
</v-card-text>
@@ -289,13 +285,12 @@ async function onNormCommit() {
normCommitting.value = true
normResult.value = null
try {
// Long op (FK repoints): enqueue the maintenance task, then tail the
// activity dashboard until its row reaches a terminal status. (The
// per-run summary dict isn't exposed by /activity/runs, so we surface
// the terminal status — the dry-run preview is the detailed view.)
const { task_id: taskId } = await store.normalizeTags({ dryRun: false })
const row = await store.pollTaskUntilDone(taskId)
normResult.value = row?.status || 'ok'
// Fire-and-forget: the task is time-boxed and self-resuming across chunks
// (a large back-catalog can't finish in one run), so we DON'T poll-until-
// done — that would falsely report "complete" after the first chunk. Just
// confirm it's queued; the operator can re-run Preview later to verify.
await store.normalizeTags({ dryRun: false })
normResult.value = 'queued'
normPreview.value = { total_changes: 0, tags_to_rename: 0, collisions: 0, tags_to_merge: 0, sample: [] }
} finally {
normCommitting.value = false