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>
This commit is contained in:
@@ -1,49 +1,50 @@
|
||||
<template>
|
||||
<v-card class="fc-clean-card">
|
||||
<CardHeading icon="mdi-image-size-select-small" title="Minimum dimensions" />
|
||||
<v-card-text>
|
||||
<p class="fc-muted text-body-2 mb-3">
|
||||
Find and delete images smaller than the threshold. Mirrors the
|
||||
import-time <code>min_width</code> / <code>min_height</code>
|
||||
filter, applied retroactively to the existing library.
|
||||
</p>
|
||||
<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-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
|
||||
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>
|
||||
</v-card-text>
|
||||
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"
|
||||
@@ -55,7 +56,7 @@
|
||||
:description="`Width < ${minW} OR height < ${minH}`"
|
||||
@confirm="onConfirmedDelete"
|
||||
/>
|
||||
</v-card>
|
||||
</MaintenanceTile>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -64,7 +65,7 @@ import { onMounted, ref } from 'vue'
|
||||
|
||||
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
||||
import { useCleanupStore } from '../../stores/cleanup.js'
|
||||
import CardHeading from '../common/CardHeading.vue'
|
||||
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
|
||||
@@ -116,7 +117,3 @@ async function onConfirmedDelete(token) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-clean-card { border-radius: 8px; }
|
||||
</style>
|
||||
|
||||
@@ -1,79 +1,82 @@
|
||||
<template>
|
||||
<v-card class="fc-clean-card">
|
||||
<CardHeading icon="mdi-palette-swatch" title="Single-color audit" />
|
||||
<v-card-text>
|
||||
<p class="fc-muted text-body-2 mb-3">
|
||||
Scan library for images dominated by one color within the
|
||||
tolerance. Catches placeholder / solid-fill / error-page images
|
||||
that slipped through the import filter. Same background-scan
|
||||
cadence as the transparency audit.
|
||||
</p>
|
||||
<MaintenanceTile
|
||||
icon="mdi-palette-swatch"
|
||||
title="Single-color audit"
|
||||
blurb="Find & delete near-solid / placeholder images."
|
||||
destructive
|
||||
:open="audit?.status === 'running'"
|
||||
>
|
||||
<p class="fc-muted text-body-2 mb-3">
|
||||
Scan library for images dominated by one color within the
|
||||
tolerance. Catches placeholder / solid-fill / error-page images
|
||||
that slipped through the import filter. Same background-scan
|
||||
cadence as the transparency audit.
|
||||
</p>
|
||||
|
||||
<v-row dense>
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
v-model.number="threshold" label="Threshold (0–1)"
|
||||
type="number" min="0" max="1" step="0.01"
|
||||
density="compact" hide-details
|
||||
:disabled="audit && audit.status === 'running'"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
v-model.number="tolerance" label="Color tolerance (0–441)"
|
||||
type="number" min="0" max="441"
|
||||
density="compact" hide-details
|
||||
:disabled="audit && audit.status === 'running'"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
v-model.number="threshold" label="Threshold (0–1)"
|
||||
type="number" min="0" max="1" step="0.01"
|
||||
density="compact" hide-details
|
||||
:disabled="audit && audit.status === 'running'"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
v-model.number="tolerance" label="Color tolerance (0–441)"
|
||||
type="number" min="0" max="441"
|
||||
density="compact" hide-details
|
||||
:disabled="audit && audit.status === 'running'"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-btn
|
||||
v-if="!audit || audit.status !== 'running'"
|
||||
class="mt-3"
|
||||
color="accent" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-magnify-scan"
|
||||
:loading="busy"
|
||||
@click="onStart"
|
||||
>Scan library</v-btn>
|
||||
<v-btn
|
||||
v-if="!audit || audit.status !== 'running'"
|
||||
class="mt-3"
|
||||
color="accent" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-magnify-scan"
|
||||
:loading="busy"
|
||||
@click="onStart"
|
||||
>Scan library</v-btn>
|
||||
|
||||
<div v-if="audit && audit.status === 'running'" class="mt-3">
|
||||
<v-progress-linear indeterminate color="accent" />
|
||||
<div class="text-body-2 mt-2 d-flex align-center" style="gap: 10px;">
|
||||
<span>
|
||||
Scanning… {{ audit.scanned_count }} checked,
|
||||
{{ audit.matched_count }} matched
|
||||
</span>
|
||||
<v-btn
|
||||
variant="text" size="small" color="warning" rounded="pill"
|
||||
@click="onCancel"
|
||||
>Cancel</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="audit && audit.status === 'ready'" class="mt-3">
|
||||
<p class="text-body-2 mb-2">
|
||||
Scan complete. <strong>{{ audit.matched_count }}</strong>
|
||||
image(s) match.
|
||||
</p>
|
||||
<div v-if="audit && audit.status === 'running'" class="mt-3">
|
||||
<v-progress-linear indeterminate color="accent" />
|
||||
<div class="text-body-2 mt-2 d-flex align-center" style="gap: 10px;">
|
||||
<span>
|
||||
Scanning… {{ audit.scanned_count }} checked,
|
||||
{{ audit.matched_count }} matched
|
||||
</span>
|
||||
<v-btn
|
||||
v-if="audit.matched_count > 0"
|
||||
color="error" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-delete"
|
||||
@click="onApplyClick"
|
||||
>Delete {{ audit.matched_count }} matching...</v-btn>
|
||||
variant="text" size="small" color="warning" rounded="pill"
|
||||
@click="onCancel"
|
||||
>Cancel</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="audit && audit.status === 'error'"
|
||||
type="error" variant="tonal" density="compact" class="mt-3"
|
||||
>Scan failed: {{ audit.error }}</v-alert>
|
||||
<div v-if="audit && audit.status === 'ready'" class="mt-3">
|
||||
<p class="text-body-2 mb-2">
|
||||
Scan complete. <strong>{{ audit.matched_count }}</strong>
|
||||
image(s) match.
|
||||
</p>
|
||||
<v-btn
|
||||
v-if="audit.matched_count > 0"
|
||||
color="error" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-delete"
|
||||
@click="onApplyClick"
|
||||
>Delete {{ audit.matched_count }} matching...</v-btn>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="audit && audit.status === 'applied'"
|
||||
type="success" variant="tonal" density="compact" class="mt-3"
|
||||
>Applied — matched images deleted.</v-alert>
|
||||
</v-card-text>
|
||||
<v-alert
|
||||
v-if="audit && audit.status === 'error'"
|
||||
type="error" variant="tonal" density="compact" class="mt-3"
|
||||
>Scan failed: {{ audit.error }}</v-alert>
|
||||
|
||||
<v-alert
|
||||
v-if="audit && audit.status === 'applied'"
|
||||
type="success" variant="tonal" density="compact" class="mt-3"
|
||||
>Applied — matched images deleted.</v-alert>
|
||||
|
||||
<DestructiveConfirmModal
|
||||
v-if="audit"
|
||||
@@ -86,7 +89,7 @@
|
||||
description="Permanently deletes images matched by the single-color scan."
|
||||
@confirm="onConfirmedApply"
|
||||
/>
|
||||
</v-card>
|
||||
</MaintenanceTile>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -94,7 +97,7 @@ import { toast } from '../../utils/toast.js'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
||||
import CardHeading from '../common/CardHeading.vue'
|
||||
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
||||
import { useCleanupStore } from '../../stores/cleanup.js'
|
||||
|
||||
const store = useCleanupStore()
|
||||
@@ -185,7 +188,3 @@ async function onConfirmedApply(token) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-clean-card { border-radius: 8px; }
|
||||
</style>
|
||||
|
||||
@@ -1,66 +1,69 @@
|
||||
<template>
|
||||
<v-card class="fc-clean-card">
|
||||
<CardHeading icon="mdi-checkerboard" title="Transparency audit" />
|
||||
<v-card-text>
|
||||
<p class="fc-muted text-body-2 mb-3">
|
||||
Scan library for images whose transparent-pixel fraction exceeds
|
||||
the threshold. Animated WebPs / GIFs are skipped (the import-side
|
||||
rule does the same). Runs as a background task — ~50ms per image,
|
||||
so a 57k library takes ~50 minutes.
|
||||
</p>
|
||||
<MaintenanceTile
|
||||
icon="mdi-checkerboard"
|
||||
title="Transparency audit"
|
||||
blurb="Find & delete images that are mostly transparent."
|
||||
destructive
|
||||
:open="audit?.status === 'running'"
|
||||
>
|
||||
<p class="fc-muted text-body-2 mb-3">
|
||||
Scan library for images whose transparent-pixel fraction exceeds
|
||||
the threshold. Animated WebPs / GIFs are skipped (the import-side
|
||||
rule does the same). Runs as a background task — ~50ms per image,
|
||||
so a 57k library takes ~50 minutes.
|
||||
</p>
|
||||
|
||||
<v-text-field
|
||||
v-model.number="threshold" label="Transparency threshold (0–1)"
|
||||
type="number" min="0" max="1" step="0.01" density="compact" hide-details
|
||||
:disabled="audit && audit.status === 'running'"
|
||||
class="mb-3"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model.number="threshold" label="Transparency threshold (0–1)"
|
||||
type="number" min="0" max="1" step="0.01" density="compact" hide-details
|
||||
:disabled="audit && audit.status === 'running'"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
v-if="!audit || audit.status !== 'running'"
|
||||
color="accent" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-magnify-scan"
|
||||
:loading="busy"
|
||||
@click="onStart"
|
||||
>Scan library</v-btn>
|
||||
<v-btn
|
||||
v-if="!audit || audit.status !== 'running'"
|
||||
color="accent" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-magnify-scan"
|
||||
:loading="busy"
|
||||
@click="onStart"
|
||||
>Scan library</v-btn>
|
||||
|
||||
<div v-if="audit && audit.status === 'running'" class="mt-3">
|
||||
<v-progress-linear indeterminate color="accent" />
|
||||
<div class="text-body-2 mt-2 d-flex align-center" style="gap: 10px;">
|
||||
<span>
|
||||
Scanning… {{ audit.scanned_count }} checked,
|
||||
{{ audit.matched_count }} matched
|
||||
</span>
|
||||
<v-btn
|
||||
variant="text" size="small" color="warning" rounded="pill"
|
||||
@click="onCancel"
|
||||
>Cancel</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="audit && audit.status === 'ready'" class="mt-3">
|
||||
<p class="text-body-2 mb-2">
|
||||
Scan complete. <strong>{{ audit.matched_count }}</strong>
|
||||
image(s) match.
|
||||
</p>
|
||||
<div v-if="audit && audit.status === 'running'" class="mt-3">
|
||||
<v-progress-linear indeterminate color="accent" />
|
||||
<div class="text-body-2 mt-2 d-flex align-center" style="gap: 10px;">
|
||||
<span>
|
||||
Scanning… {{ audit.scanned_count }} checked,
|
||||
{{ audit.matched_count }} matched
|
||||
</span>
|
||||
<v-btn
|
||||
v-if="audit.matched_count > 0"
|
||||
color="error" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-delete"
|
||||
@click="onApplyClick"
|
||||
>Delete {{ audit.matched_count }} matching...</v-btn>
|
||||
variant="text" size="small" color="warning" rounded="pill"
|
||||
@click="onCancel"
|
||||
>Cancel</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="audit && audit.status === 'error'"
|
||||
type="error" variant="tonal" density="compact" class="mt-3"
|
||||
>Scan failed: {{ audit.error }}</v-alert>
|
||||
<div v-if="audit && audit.status === 'ready'" class="mt-3">
|
||||
<p class="text-body-2 mb-2">
|
||||
Scan complete. <strong>{{ audit.matched_count }}</strong>
|
||||
image(s) match.
|
||||
</p>
|
||||
<v-btn
|
||||
v-if="audit.matched_count > 0"
|
||||
color="error" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-delete"
|
||||
@click="onApplyClick"
|
||||
>Delete {{ audit.matched_count }} matching...</v-btn>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="audit && audit.status === 'applied'"
|
||||
type="success" variant="tonal" density="compact" class="mt-3"
|
||||
>Applied — matched images deleted.</v-alert>
|
||||
</v-card-text>
|
||||
<v-alert
|
||||
v-if="audit && audit.status === 'error'"
|
||||
type="error" variant="tonal" density="compact" class="mt-3"
|
||||
>Scan failed: {{ audit.error }}</v-alert>
|
||||
|
||||
<v-alert
|
||||
v-if="audit && audit.status === 'applied'"
|
||||
type="success" variant="tonal" density="compact" class="mt-3"
|
||||
>Applied — matched images deleted.</v-alert>
|
||||
|
||||
<DestructiveConfirmModal
|
||||
v-if="audit"
|
||||
@@ -73,7 +76,7 @@
|
||||
description="Permanently deletes images matched by the transparency scan."
|
||||
@confirm="onConfirmedApply"
|
||||
/>
|
||||
</v-card>
|
||||
</MaintenanceTile>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -81,7 +84,7 @@ import { toast } from '../../utils/toast.js'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
||||
import CardHeading from '../common/CardHeading.vue'
|
||||
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
||||
import { useCleanupStore } from '../../stores/cleanup.js'
|
||||
|
||||
const store = useCleanupStore()
|
||||
@@ -168,7 +171,3 @@ async function onConfirmedApply(token) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-clean-card { border-radius: 8px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user