3b435dc0ba
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>
120 lines
3.4 KiB
Vue
120 lines
3.4 KiB
Vue
<template>
|
|
<MaintenanceTile
|
|
icon="mdi-image-size-select-small"
|
|
title="Minimum dimensions"
|
|
blurb="Delete images below the import min-width / min-height threshold."
|
|
destructive
|
|
>
|
|
<p class="fc-muted text-body-2 mb-3">
|
|
Mirrors the import-time <code>min_width</code> / <code>min_height</code>
|
|
filter, applied retroactively to the existing library.
|
|
</p>
|
|
|
|
<v-row dense>
|
|
<v-col cols="6">
|
|
<v-text-field
|
|
v-model.number="minW" label="Min width (px)" type="number"
|
|
min="0" density="compact" hide-details
|
|
/>
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<v-text-field
|
|
v-model.number="minH" label="Min height (px)" type="number"
|
|
min="0" density="compact" hide-details
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<div class="d-flex align-center mt-3" style="gap: 10px;">
|
|
<v-btn
|
|
color="accent" variant="flat" rounded="pill"
|
|
prepend-icon="mdi-magnify"
|
|
:loading="busy"
|
|
@click="onPreview"
|
|
>Preview</v-btn>
|
|
|
|
<span v-if="preview" class="text-body-2">
|
|
<strong>{{ preview.count }}</strong> image(s) would be deleted.
|
|
</span>
|
|
</div>
|
|
|
|
<v-btn
|
|
v-if="preview && preview.count > 0"
|
|
class="mt-3"
|
|
color="error" variant="flat" rounded="pill"
|
|
prepend-icon="mdi-delete"
|
|
@click="onDeleteClick"
|
|
>Delete {{ preview.count }} matching...</v-btn>
|
|
|
|
<DestructiveConfirmModal
|
|
v-model="showModal"
|
|
action="delete"
|
|
kind="min-dim"
|
|
tier="C"
|
|
:expected-token-override="preview?.confirm_token || ''"
|
|
:projected-counts="projectedCounts"
|
|
:description="`Width < ${minW} OR height < ${minH}`"
|
|
@confirm="onConfirmedDelete"
|
|
/>
|
|
</MaintenanceTile>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { toast } from '../../utils/toast.js'
|
|
import { onMounted, ref } from 'vue'
|
|
|
|
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
|
import { useCleanupStore } from '../../stores/cleanup.js'
|
|
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
|
|
|
// Backend's preview response hands the full Tier-C confirm token back
|
|
// as `confirm_token` (e.g. `delete-min-dim-1a2b3c4d`); passed straight
|
|
// to the modal via `expected-token-override`. We previously
|
|
// reconstructed via Web Crypto's SHA-256, but `crypto.subtle` is
|
|
// Secure-Context-gated and undefined on plain-HTTP origins, so the
|
|
// Delete button silently swallowed the TypeError. Operator-flagged
|
|
// 2026-05-27.
|
|
|
|
const store = useCleanupStore()
|
|
const minW = ref(0)
|
|
const minH = ref(0)
|
|
const preview = ref(null)
|
|
const busy = ref(false)
|
|
const showModal = ref(false)
|
|
const projectedCounts = ref({})
|
|
|
|
onMounted(async () => {
|
|
await store.loadDefaults()
|
|
minW.value = store.defaults.min_width
|
|
minH.value = store.defaults.min_height
|
|
})
|
|
|
|
async function onPreview() {
|
|
busy.value = true
|
|
try {
|
|
preview.value = await store.previewMinDim(minW.value, minH.value)
|
|
} catch (e) {
|
|
toast({ text: `Preview failed: ${e.message}`, type: 'error' })
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
function onDeleteClick() {
|
|
projectedCounts.value = { 'Images to delete': preview.value.count }
|
|
showModal.value = true
|
|
}
|
|
|
|
async function onConfirmedDelete(token) {
|
|
try {
|
|
const res = await store.deleteMinDim(minW.value, minH.value, token)
|
|
toast({
|
|
text: `Deleted ${res.deleted} image(s)`, type: 'success',
|
|
})
|
|
preview.value = null
|
|
} catch (e) {
|
|
toast({ text: `Delete failed: ${e.message}`, type: 'error' })
|
|
}
|
|
}
|
|
</script>
|