CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 4s
CI and images / frontend-build (push) Successful in 25s
CI and images / extension-test (push) Successful in 28s
CI and images / backend-lint-and-test (push) Successful in 34s
CI and images / integration (push) Failing after 2m24s
CI and images / sign-extension (push) Skipped
CI and images / build-web (push) Skipped
CI and images / smoke-web (push) Skipped
CI and images / promote (push) Skipped
CI and images / build-agent (push) Skipped
celery_signals._queue_for was a hand-kept copy of task_routes and had drifted: - backup, admin and library_audit jobs, and backfill_phash, run on maintenance_long but were recorded as `maintenance`; - translation and gpu_queue jobs were recorded as `default`, where the 5-minute stall sweep failed healthy 35-minute translation runs. It now asks the router, cached per task name. A new guard test checks every registered task's hard time limit against the stall threshold the sweep would use for it. It also caught these sweeps, which failed healthy runs mid-flight and are fixed here: - ml's scheduled sweeps (35 min, previously swept at 25); - train_heads and apply_head_tags (65 min); - import_media_file (6 min, previously swept at 5); - the long lane, which now has its own 45-minute threshold. UI changes: - the admin job poller follows a job by celery_task_id, via a new filter on /runs, instead of by lane; - the archive re-extract and missing-file repair cards show the long lane's backlog, where their jobs actually wait; - the queue table lists maintenance_long. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
162 lines
5.4 KiB
JavaScript
162 lines
5.4 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
|
|
import { useApi } from '../composables/useApi.js'
|
|
|
|
export const useAdminStore = defineStore('admin', () => {
|
|
const api = useApi()
|
|
const lastError = ref(null)
|
|
|
|
// Every admin action runs through _guard: reset lastError, run the call,
|
|
// surface its message to lastError on failure, and re-throw so callers still
|
|
// see the rejection. One copy of the capture/rethrow instead of 13.
|
|
async function _guard(fn) {
|
|
lastError.value = null
|
|
try {
|
|
return await fn()
|
|
} catch (e) {
|
|
lastError.value = e.message
|
|
throw e
|
|
}
|
|
}
|
|
|
|
// The Tier-A maintenance endpoints share one shape: POST a dry_run flag
|
|
// (default the SAFE preview), optionally with extra body fields. The backend's
|
|
// same predicate drives preview + apply, so the UI just toggles dryRun.
|
|
function _dryRunPost(url, { dryRun = true, ...extra } = {}) {
|
|
return _guard(() => api.post(url, { body: { dry_run: dryRun, ...extra } }))
|
|
}
|
|
|
|
// --- Tier-C: artist cascade ---------------------------------------
|
|
|
|
function projectArtistCascade(slug) {
|
|
return _guard(() => api.post(
|
|
`/api/admin/artists/${encodeURIComponent(slug)}/cascade-delete`,
|
|
{ body: { dry_run: true } },
|
|
))
|
|
}
|
|
|
|
function dispatchArtistCascade(slug, confirm) {
|
|
return _guard(() => api.post(
|
|
`/api/admin/artists/${encodeURIComponent(slug)}/cascade-delete`,
|
|
{ body: { dry_run: false, confirm } },
|
|
))
|
|
}
|
|
|
|
// --- Tier-C: bulk image delete ------------------------------------
|
|
|
|
function projectBulkImageDelete(imageIds) {
|
|
return _guard(() => api.post(
|
|
'/api/admin/images/bulk-delete',
|
|
{ body: { image_ids: imageIds, dry_run: true } },
|
|
))
|
|
}
|
|
|
|
function dispatchBulkImageDelete(imageIds, confirm) {
|
|
return _guard(() => api.post(
|
|
'/api/admin/images/bulk-delete',
|
|
{ body: { image_ids: imageIds, dry_run: false, confirm } },
|
|
))
|
|
}
|
|
|
|
// --- Tier-B: tag delete + merge -----------------------------------
|
|
|
|
function deleteTag(tagId) {
|
|
return _guard(() => api.delete(`/api/admin/tags/${tagId}`))
|
|
}
|
|
|
|
// dryRun → {preview: {...}} (non-mutating counts + sample); else {result}.
|
|
function mergeTags(destId, sourceId, { dryRun = false } = {}) {
|
|
return _guard(() => api.post(
|
|
`/api/admin/tags/${destId}/merge`,
|
|
{ body: { source_id: sourceId, dry_run: dryRun } },
|
|
))
|
|
}
|
|
|
|
async function tagUsageCount(tagId) {
|
|
return _guard(async () => {
|
|
const body = await api.get(`/api/admin/tags/${tagId}/usage-count`)
|
|
return body.count
|
|
})
|
|
}
|
|
|
|
// --- Tier-A: dry-run/apply maintenance ----------------------------
|
|
|
|
function pruneUnusedTags(opts = {}) {
|
|
return _dryRunPost('/api/admin/tags/prune-unused', opts)
|
|
}
|
|
|
|
// Delete bare posts (no images + no attachments) — the empty-post flood
|
|
// shells. Preview/apply parity on the backend; UI previews then confirms.
|
|
function pruneBarePosts(opts = {}) {
|
|
return _dryRunPost('/api/admin/posts/prune-bare', opts)
|
|
}
|
|
|
|
// Unify duplicate post rows (gallery-dl attachment-id + native post-id for the
|
|
// same real post) onto one post-id-keyed keeper. Images untouched. sourceId
|
|
// (optional) scopes to one source; sent as source_id in the body.
|
|
function reconcileDuplicatePosts({ sourceId = null, ...opts } = {}) {
|
|
return _dryRunPost('/api/admin/posts/reconcile-duplicates', {
|
|
...opts, source_id: sourceId,
|
|
})
|
|
}
|
|
|
|
// Destructive whole-instance reset: deletes ALL general + character tags AND
|
|
// their applications (the heads' training data included) — fandom + series
|
|
// preserved. dry-run returns a `confirm` token; the apply must pass it back
|
|
// ({ dryRun: false, confirm }) or the server rejects it.
|
|
function resetContentTagging(opts = {}) {
|
|
return _dryRunPost('/api/admin/tags/reset-content', opts)
|
|
}
|
|
|
|
// #714: Title-Case the back-catalog + merge case/whitespace-variant tags.
|
|
// dry-run returns a projection inline; live returns {task_id} (long op —
|
|
// FK repoints) the caller polls via pollTaskUntilDone.
|
|
function normalizeTags(opts = {}) {
|
|
return _dryRunPost('/api/admin/tags/normalize', opts)
|
|
}
|
|
|
|
// --- Task progress polling (taps FC-3i activity dashboard) --------
|
|
|
|
/**
|
|
* Polls /api/system/activity/runs?celery_task_id=<id> every 3s,
|
|
* resolves when a task_run row with the given celery task_id
|
|
* reaches a terminal status (ok / error / timeout). Returns the
|
|
* row. Times out after 30 min by default.
|
|
*/
|
|
async function pollTaskUntilDone(taskId, { timeoutMs = 1_800_000 } = {}) {
|
|
const deadline = Date.now() + timeoutMs
|
|
while (Date.now() < deadline) {
|
|
const body = await api.get(
|
|
'/api/system/activity/runs',
|
|
// By id, not by lane: these jobs run on `maintenance_long`, and a
|
|
// lane filter here is one more copy of the routing table (#4432).
|
|
{ params: { celery_task_id: taskId, limit: 1 } },
|
|
)
|
|
const row = (body.runs || []).find(r => r.celery_task_id === taskId)
|
|
if (row && ['ok', 'error', 'timeout'].includes(row.status)) {
|
|
return row
|
|
}
|
|
await new Promise((r) => setTimeout(r, 3000))
|
|
}
|
|
throw new Error(`task ${taskId} did not finish within ${timeoutMs}ms`)
|
|
}
|
|
|
|
return {
|
|
lastError,
|
|
projectArtistCascade,
|
|
dispatchArtistCascade,
|
|
projectBulkImageDelete,
|
|
dispatchBulkImageDelete,
|
|
deleteTag,
|
|
mergeTags,
|
|
tagUsageCount,
|
|
pruneUnusedTags,
|
|
pruneBarePosts,
|
|
reconcileDuplicatePosts,
|
|
resetContentTagging,
|
|
normalizeTags,
|
|
pollTaskUntilDone,
|
|
}
|
|
})
|