From e41ab1cca5e37526479e247a9bbe0051f2cd2ea5 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 25 May 2026 00:47:09 -0400 Subject: [PATCH] =?UTF-8?q?fc3k(ui):=20Pinia=20admin=20store=20=E2=80=94?= =?UTF-8?q?=20six=20endpoints=20+=20task=5Frun=20polling=20for=20Tier-C=20?= =?UTF-8?q?dispatched=20ops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/stores/admin.js | 153 +++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 frontend/src/stores/admin.js diff --git a/frontend/src/stores/admin.js b/frontend/src/stores/admin.js new file mode 100644 index 0000000..37eaf8c --- /dev/null +++ b/frontend/src/stores/admin.js @@ -0,0 +1,153 @@ +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) + + // --- Tier-C: artist cascade --------------------------------------- + + async function projectArtistCascade(slug) { + lastError.value = null + try { + return await api.post( + `/api/admin/artists/${encodeURIComponent(slug)}/cascade-delete`, + { body: { dry_run: true } }, + ) + } catch (e) { + lastError.value = e.message + throw e + } + } + + async function dispatchArtistCascade(slug, confirm) { + lastError.value = null + try { + return await api.post( + `/api/admin/artists/${encodeURIComponent(slug)}/cascade-delete`, + { body: { dry_run: false, confirm } }, + ) + } catch (e) { + lastError.value = e.message + throw e + } + } + + // --- Tier-C: bulk image delete ------------------------------------ + + async function projectBulkImageDelete(imageIds) { + lastError.value = null + try { + return await api.post( + '/api/admin/images/bulk-delete', + { body: { image_ids: imageIds, dry_run: true } }, + ) + } catch (e) { + lastError.value = e.message + throw e + } + } + + async function dispatchBulkImageDelete(imageIds, confirm) { + lastError.value = null + try { + return await api.post( + '/api/admin/images/bulk-delete', + { body: { image_ids: imageIds, dry_run: false, confirm } }, + ) + } catch (e) { + lastError.value = e.message + throw e + } + } + + // --- Tier-B: tag delete + merge ----------------------------------- + + async function deleteTag(tagId) { + lastError.value = null + try { + return await api.delete(`/api/admin/tags/${tagId}`) + } catch (e) { + lastError.value = e.message + throw e + } + } + + async function mergeTags(destId, sourceId) { + lastError.value = null + try { + return await api.post( + `/api/admin/tags/${destId}/merge`, + { body: { source_id: sourceId } }, + ) + } catch (e) { + lastError.value = e.message + throw e + } + } + + async function tagUsageCount(tagId) { + lastError.value = null + try { + const body = await api.get(`/api/admin/tags/${tagId}/usage-count`) + return body.count + } catch (e) { + lastError.value = e.message + throw e + } + } + + // --- Tier-A: prune unused ----------------------------------------- + + async function pruneUnusedTags({ dryRun = true } = {}) { + lastError.value = null + try { + return await api.post( + '/api/admin/tags/prune-unused', + { body: { dry_run: dryRun } }, + ) + } catch (e) { + lastError.value = e.message + throw e + } + } + + // --- 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, + pollTaskUntilDone, + } +})