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?queue=maintenance 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', { params: { queue: 'maintenance', limit: 20 } }, ) 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, } })