Files
FabledCurator/frontend/src/components/settings/GatedPurgeCard.vue
T
bvandeusen 3b435dc0ba
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m14s
feat(settings): tidy Cleanup tab into sectioned compact tiles (pass 1)
The Cleanup + Maintenance sections had ~17 full-width stacked cards with long
descriptions — a hunt to scan. Operator wants compact, sectioned, scannable tiles
(2026-06-18: keep both tabs, group inside, compact tiles in a grid).

New common/MaintenanceTile.vue: a compact expandable tile (icon + short title +
one-line blurb collapsed; click the header to expand the full controls/preview/
result inline; keyboard-accessible button + focus ring;  tints the
icon,  keeps a running task expanded).

Cleanup tab (this pass) restructured into 3 sections — Import-filter audits
(Min dimensions, Transparency, Single-color) / Duplicates & posts (Bare posts,
Duplicate posts, Deduplicate videos, Gated-post previews) / Tags (Unused, Legacy,
Reset content tagging, Standardize casing) — each a responsive grid of tiles.
PostMaintenanceCard split into 2 tiles, TagMaintenanceCard into 4. Moved
VideoDedupCard + GatedPurgeCard from the Maintenance tab here (both are
destructive content cleanup). All card logic unchanged — only the chrome.

Maintenance tab tiling is pass 2 (TODO noted in MaintenancePanel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:48:18 -04:00

139 lines
5.5 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). -->
<MaintenanceTile
icon="mdi-image-off"
title="Gated-post previews"
blurb="Delete blurred locked-preview images from tier-gated posts."
destructive
:open="applying || previewing"
>
<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-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>
</MaintenanceTile>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useMaintenanceTask } from '../../composables/useMaintenanceTask.js'
import MaintenanceTile from '../common/MaintenanceTile.vue'
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>