Files
FabledCurator/frontend/src/components/settings/GatedPurgeCard.vue
T
bvandeusen e287802ecb
CI / lint (push) Successful in 4s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m16s
fix(maint): resurface dedup/gated-purge results after navigate-away (#877)
Long-running maintenance tasks must survive navigating away or reloading
the page. VideoDedupCard + GatedPurgeCard held the in-flight Celery task id
only in component refs and polled task-result inline, so leaving the page
mid-run lost the id and the result was never shown — even though the task
finished on the worker.

New shared composable useMaintenanceTask: persists {taskId, mode, startedAt}
to localStorage on dispatch, re-attaches on mount, and re-shows the result
when the task finishes (the celery result backend retains the summary well
under result_expires). Stale-guard skips resume past 3h. Both cards refactored
onto it; card-specific computeds + confirm dialog kept.

Also fixed the QueueStatusBar lane: both cards watched queue="maintenance"
but tasks.admin.* routes to maintenance_long, so the bar never reflected
their own task — now queue="maintenance_long".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:38:32 -04:00

135 lines
5.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- #874 follow-up: purge blurred locked-preview images grabbed from
tier-gated Patreon posts before the ingester fix. Re-walks each feed and
matches by content hash, so real content downloaded when access existed is
provably spared. Preview first, then apply (destructive: deletes the
matched blurred-preview files). -->
<v-card>
<v-card-title>Clean up gated-post previews</v-card-title>
<v-card-text>
<p class="text-body-2 mb-3">
Removes the blurred locked-preview images that were grabbed from
tier-gated Patreon posts before they were filtered out. It re-walks every
enabled Patreon source and matches by exact content hash, so anything you
downloaded while you <em>had</em> access is left untouched
only the blurred previews are removed. <strong>Preview</strong> first to
see the count; <strong>Apply</strong> deletes the matched files and clears
their download ledger so the real media can re-download if you regain
access. The re-walk can take a while across many sources.
</p>
<div class="d-flex align-center flex-wrap" style="gap: 12px;">
<v-btn
color="primary" variant="tonal" rounded="pill"
:loading="previewing" :disabled="applying" @click="preview"
>
<v-icon start>mdi-magnify</v-icon> Preview
</v-btn>
<v-btn
color="error" rounded="pill"
:loading="applying"
:disabled="previewing || !canApply"
@click="confirmOpen = true"
>
<v-icon start>mdi-image-off</v-icon> Apply
</v-btn>
</div>
<v-alert
v-if="summary" :type="summaryType" variant="tonal" class="mt-4"
density="comfortable"
>
<span v-if="applied">
Removed {{ summary.deleted }} blurred preview image(s) across
{{ summary.gated_posts }} gated post(s); reclaimed
{{ humanBytes(summary.reclaim_bytes) }};
removed {{ summary.posts_deleted }} now-empty post(s).
<template v-if="summary.unverifiable > 0">
{{ summary.unverifiable }} image(s) couldn't be verified (no stored
source hash) and were kept.
</template>
</span>
<span v-else-if="summary.matched > 0">
{{ summary.matched }} blurred preview image(s) across
{{ summary.gated_posts }} gated post(s)
{{ humanBytes(summary.reclaim_bytes) }} reclaimable. Click
<strong>Apply</strong> to delete them.
<template v-if="summary.unverifiable > 0">
({{ summary.unverifiable }} image(s) on gated posts have no stored
source hash, so they can't be verified and will be kept.)
</template>
</span>
<span v-else>
No blurred gated-post previews found
<template v-if="summary.gated_posts > 0">
across {{ summary.gated_posts }} gated post(s)</template>.
</span>
<template v-if="summary.partial">
<br>
<small>
Scanned {{ summary.sources_scanned }}/{{ summary.sources_total }}
sources before the time budget run again to finish the rest.
</small>
</template>
</v-alert>
<QueueStatusBar queue="maintenance_long" queue-label="Maintenance" />
</v-card-text>
<v-dialog v-model="confirmOpen" max-width="460">
<v-card>
<v-card-title>Delete gated-post previews?</v-card-title>
<v-card-text class="text-body-2">
This permanently deletes
<strong>{{ summary?.matched ?? 0 }}</strong> blurred preview file(s)
matched by exact content hash. Real content you downloaded with access
has a different hash and is not affected.
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="confirmOpen = false">Cancel</v-btn>
<v-btn color="error" @click="apply">Delete previews</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useMaintenanceTask } from '../../composables/useMaintenanceTask.js'
import QueueStatusBar from './QueueStatusBar.vue'
const confirmOpen = ref(false)
// The re-walk hits every enabled Patreon feed and can run long — the lifecycle +
// resurface-after-navigation live in the shared composable. 300 polls × 2s ≈ 10m.
const { previewing, applying, summary, applied, preview, apply: applyTask } = useMaintenanceTask({
endpoint: '/api/admin/maintenance/purge-gated-previews',
storageKey: 'fc.maint.gatedPurge',
appliedToast: 'Gated-post previews removed',
maxPolls: 300,
})
const canApply = computed(() => !!summary.value && !applied.value && summary.value.matched > 0)
const summaryType = computed(() => {
if (applied.value) return 'success'
return summary.value && summary.value.matched > 0 ? 'info' : 'success'
})
function humanBytes (n) {
const b = Number(n || 0)
if (b >= 1 << 30) return (b / (1 << 30)).toFixed(1) + ' GB'
if (b >= 1 << 20) return (b / (1 << 20)).toFixed(1) + ' MB'
if (b >= 1 << 10) return (b / (1 << 10)).toFixed(1) + ' KB'
return b + ' B'
}
// The confirm dialog gates the destructive apply; close it, then run.
function apply () {
confirmOpen.value = false
applyTask()
}
</script>