feat(fc-cleanup): Pinia store + 3 cards + CleanupView + SettingsView tab + TagMaintenanceCard moved from Maintenance + ruff lint fixes — Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,11 @@ api_bp.add_url_rule("/health", view_func=health.get_health, methods=["GET"])
|
||||
def all_blueprints() -> list[Blueprint]:
|
||||
from .admin import admin_bp
|
||||
from .aliases import aliases_bp
|
||||
from .cleanup import cleanup_bp
|
||||
from .allowlist import allowlist_bp
|
||||
from .artist import artist_bp
|
||||
from .artists import artists_bp
|
||||
from .attachments import attachments_bp
|
||||
from .cleanup import cleanup_bp
|
||||
from .credentials import credentials_bp
|
||||
from .downloads import downloads_bp
|
||||
from .extension import extension_bp
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<v-card class="fc-clean-card">
|
||||
<v-card-title class="d-flex align-center" style="gap: 10px;">
|
||||
<v-icon icon="mdi-image-size-select-small" size="small" />
|
||||
<span>Minimum dimensions</span>
|
||||
</v-card-title>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</v-card-text>
|
||||
|
||||
<DestructiveConfirmModal
|
||||
v-model="showModal"
|
||||
action="delete"
|
||||
kind="min-dim"
|
||||
:run-id="tokenSha8"
|
||||
tier="C"
|
||||
:projected-counts="projectedCounts"
|
||||
:description="`Width < ${minW} OR height < ${minH}`"
|
||||
@confirm="onConfirmedDelete"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
||||
import { useCleanupStore } from '../../stores/cleanup.js'
|
||||
|
||||
const store = useCleanupStore()
|
||||
const minW = ref(0)
|
||||
const minH = ref(0)
|
||||
const preview = ref(null)
|
||||
const busy = ref(false)
|
||||
const showModal = ref(false)
|
||||
const tokenSha8 = ref('')
|
||||
const projectedCounts = ref({})
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadDefaults()
|
||||
minW.value = store.defaults.min_width
|
||||
minH.value = store.defaults.min_height
|
||||
})
|
||||
|
||||
// SHA-256 truncated to 8 hex chars — matches the backend's
|
||||
// _min_dim_token() exactly. Web Crypto rejects MD5 as insecure.
|
||||
async function sha8(canon) {
|
||||
const enc = new TextEncoder()
|
||||
const buf = await crypto.subtle.digest('SHA-256', enc.encode(canon))
|
||||
const hex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
return hex.slice(0, 8)
|
||||
}
|
||||
|
||||
async function onPreview() {
|
||||
busy.value = true
|
||||
try {
|
||||
preview.value = await store.previewMinDim(minW.value, minH.value)
|
||||
} catch (e) {
|
||||
window.__fcToast?.({ text: `Preview failed: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteClick() {
|
||||
tokenSha8.value = await sha8(`${minW.value}x${minH.value}`)
|
||||
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)
|
||||
window.__fcToast?.({
|
||||
text: `Deleted ${res.deleted} image(s)`, type: 'success',
|
||||
})
|
||||
preview.value = null
|
||||
} catch (e) {
|
||||
window.__fcToast?.({ text: `Delete failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-clean-card { border-radius: 8px; }
|
||||
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<v-card class="fc-clean-card">
|
||||
<v-card-title class="d-flex align-center" style="gap: 10px;">
|
||||
<v-icon icon="mdi-palette-swatch" size="small" />
|
||||
<span>Single-color audit</span>
|
||||
</v-card-title>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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 === '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>
|
||||
</v-card-text>
|
||||
|
||||
<DestructiveConfirmModal
|
||||
v-if="audit"
|
||||
v-model="showModal"
|
||||
action="delete"
|
||||
kind="audit"
|
||||
:run-id="audit.id"
|
||||
tier="C"
|
||||
:projected-counts="projectedCounts"
|
||||
description="Permanently deletes images matched by the single-color scan."
|
||||
@confirm="onConfirmedApply"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
||||
import { useCleanupStore } from '../../stores/cleanup.js'
|
||||
|
||||
const store = useCleanupStore()
|
||||
const threshold = ref(0.95)
|
||||
const tolerance = ref(30)
|
||||
const audit = ref(null)
|
||||
const busy = ref(false)
|
||||
const showModal = ref(false)
|
||||
const projectedCounts = ref({})
|
||||
let pollTimer = null
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadDefaults()
|
||||
threshold.value = store.defaults.single_color_threshold
|
||||
tolerance.value = store.defaults.single_color_tolerance
|
||||
})
|
||||
|
||||
onUnmounted(() => stopPoll())
|
||||
|
||||
function startPoll(id) {
|
||||
stopPoll()
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const fresh = await store.getAudit(id)
|
||||
audit.value = fresh
|
||||
if (fresh.status !== 'running') stopPoll()
|
||||
} catch (e) {
|
||||
stopPoll()
|
||||
window.__fcToast?.({ text: `Audit poll failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
|
||||
}
|
||||
|
||||
async function onStart() {
|
||||
busy.value = true
|
||||
try {
|
||||
const res = await store.startAudit('single_color', {
|
||||
threshold: threshold.value, tolerance: tolerance.value,
|
||||
})
|
||||
audit.value = await store.getAudit(res.audit_id)
|
||||
startPoll(res.audit_id)
|
||||
} catch (e) {
|
||||
window.__fcToast?.({ text: `Scan start failed: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onCancel() {
|
||||
if (!audit.value) return
|
||||
try {
|
||||
await store.cancelAudit(audit.value.id)
|
||||
audit.value = await store.getAudit(audit.value.id)
|
||||
stopPoll()
|
||||
} catch (e) {
|
||||
window.__fcToast?.({ text: `Cancel failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
function onApplyClick() {
|
||||
projectedCounts.value = { 'Images to delete': audit.value.matched_count }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
async function onConfirmedApply(token) {
|
||||
try {
|
||||
const res = await store.applyAudit(audit.value.id, token)
|
||||
window.__fcToast?.({
|
||||
text: `Deleted ${res.deleted} image(s)`, type: 'success',
|
||||
})
|
||||
audit.value = await store.getAudit(audit.value.id)
|
||||
} catch (e) {
|
||||
window.__fcToast?.({ text: `Apply failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-clean-card { border-radius: 8px; }
|
||||
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<v-card class="fc-clean-card">
|
||||
<v-card-title class="d-flex align-center" style="gap: 10px;">
|
||||
<v-icon icon="mdi-checkerboard" size="small" />
|
||||
<span>Transparency audit</span>
|
||||
</v-card-title>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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 === '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>
|
||||
</v-card-text>
|
||||
|
||||
<DestructiveConfirmModal
|
||||
v-if="audit"
|
||||
v-model="showModal"
|
||||
action="delete"
|
||||
kind="audit"
|
||||
:run-id="audit.id"
|
||||
tier="C"
|
||||
:projected-counts="projectedCounts"
|
||||
description="Permanently deletes images matched by the transparency scan."
|
||||
@confirm="onConfirmedApply"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
||||
import { useCleanupStore } from '../../stores/cleanup.js'
|
||||
|
||||
const store = useCleanupStore()
|
||||
const threshold = ref(0.9)
|
||||
const audit = ref(null)
|
||||
const busy = ref(false)
|
||||
const showModal = ref(false)
|
||||
const projectedCounts = ref({})
|
||||
let pollTimer = null
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadDefaults()
|
||||
threshold.value = store.defaults.transparency_threshold
|
||||
})
|
||||
|
||||
onUnmounted(() => stopPoll())
|
||||
|
||||
function startPoll(id) {
|
||||
stopPoll()
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const fresh = await store.getAudit(id)
|
||||
audit.value = fresh
|
||||
if (fresh.status !== 'running') stopPoll()
|
||||
} catch (e) {
|
||||
stopPoll()
|
||||
window.__fcToast?.({ text: `Audit poll failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
|
||||
}
|
||||
|
||||
async function onStart() {
|
||||
busy.value = true
|
||||
try {
|
||||
const res = await store.startAudit('transparency', { threshold: threshold.value })
|
||||
audit.value = await store.getAudit(res.audit_id)
|
||||
startPoll(res.audit_id)
|
||||
} catch (e) {
|
||||
window.__fcToast?.({ text: `Scan start failed: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onCancel() {
|
||||
if (!audit.value) return
|
||||
try {
|
||||
await store.cancelAudit(audit.value.id)
|
||||
audit.value = await store.getAudit(audit.value.id)
|
||||
stopPoll()
|
||||
} catch (e) {
|
||||
window.__fcToast?.({ text: `Cancel failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
function onApplyClick() {
|
||||
projectedCounts.value = { 'Images to delete': audit.value.matched_count }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
async function onConfirmedApply(token) {
|
||||
try {
|
||||
const res = await store.applyAudit(audit.value.id, token)
|
||||
window.__fcToast?.({
|
||||
text: `Deleted ${res.deleted} image(s)`, type: 'success',
|
||||
})
|
||||
audit.value = await store.getAudit(audit.value.id)
|
||||
} catch (e) {
|
||||
window.__fcToast?.({ text: `Apply failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-clean-card { border-radius: 8px; }
|
||||
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
</style>
|
||||
@@ -13,7 +13,9 @@
|
||||
<AllowlistTable class="mt-4" />
|
||||
<AliasTable class="mt-4" />
|
||||
<BackupCard class="mt-6" />
|
||||
<TagMaintenanceCard class="mt-6" />
|
||||
<!-- TagMaintenanceCard moved to Cleanup tab (v26.05.25.7) — it
|
||||
operates on the existing library which fits the Cleanup-tab
|
||||
theme, and clusters with the other audit cards. -->
|
||||
<LegacyMigrationCard class="mt-6" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,7 +27,6 @@ import MLThresholdSliders from './MLThresholdSliders.vue'
|
||||
import AllowlistTable from './AllowlistTable.vue'
|
||||
import AliasTable from './AliasTable.vue'
|
||||
import BackupCard from './BackupCard.vue'
|
||||
import TagMaintenanceCard from './TagMaintenanceCard.vue'
|
||||
import LegacyMigrationCard from './LegacyMigrationCard.vue'
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useApi } from '../composables/useApi.js'
|
||||
|
||||
export const useCleanupStore = defineStore('cleanup', () => {
|
||||
const api = useApi()
|
||||
|
||||
// Defaults sourced from ImportSettings on mount. Cards pre-fill from
|
||||
// these so the common case ("apply current import filters
|
||||
// retroactively") is one click; operator can override per-audit.
|
||||
const defaults = ref({
|
||||
min_width: 0,
|
||||
min_height: 0,
|
||||
transparency_threshold: 0.9,
|
||||
single_color_threshold: 0.95,
|
||||
single_color_tolerance: 30,
|
||||
})
|
||||
|
||||
const recentRuns = ref([])
|
||||
|
||||
async function loadDefaults() {
|
||||
const s = await api.get('/api/settings/import')
|
||||
defaults.value = {
|
||||
min_width: s.min_width ?? 0,
|
||||
min_height: s.min_height ?? 0,
|
||||
transparency_threshold: s.transparency_threshold ?? 0.9,
|
||||
single_color_threshold: s.single_color_threshold ?? 0.95,
|
||||
single_color_tolerance: s.single_color_tolerance ?? 30,
|
||||
}
|
||||
}
|
||||
|
||||
async function previewMinDim(min_width, min_height) {
|
||||
return await api.post('/api/cleanup/min-dimension/preview', {
|
||||
body: { min_width, min_height },
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteMinDim(min_width, min_height, confirm) {
|
||||
return await api.post('/api/cleanup/min-dimension/delete', {
|
||||
body: { min_width, min_height, confirm },
|
||||
})
|
||||
}
|
||||
|
||||
async function startAudit(rule, params) {
|
||||
return await api.post('/api/cleanup/audit', { body: { rule, params } })
|
||||
}
|
||||
|
||||
async function getAudit(id) {
|
||||
return await api.get(`/api/cleanup/audit/${id}`)
|
||||
}
|
||||
|
||||
async function loadHistory(limit = 20) {
|
||||
const body = await api.get(`/api/cleanup/audit?limit=${limit}`)
|
||||
recentRuns.value = body.runs
|
||||
return body.runs
|
||||
}
|
||||
|
||||
async function applyAudit(id, confirm) {
|
||||
return await api.post(`/api/cleanup/audit/${id}/apply`, { body: { confirm } })
|
||||
}
|
||||
|
||||
async function cancelAudit(id) {
|
||||
return await api.post(`/api/cleanup/audit/${id}/cancel`)
|
||||
}
|
||||
|
||||
return {
|
||||
defaults, recentRuns,
|
||||
loadDefaults,
|
||||
previewMinDim, deleteMinDim,
|
||||
startAudit, getAudit, loadHistory, applyAudit, cancelAudit,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="fc-cleanup">
|
||||
<p class="fc-muted text-body-2 mb-4">
|
||||
Retroactive enforcement of import-filter rules. Each card scans the
|
||||
existing library for content that the current import filters would
|
||||
now exclude. Destructive — typed-token confirmation required.
|
||||
</p>
|
||||
|
||||
<MinDimensionCard class="mb-4" />
|
||||
<TransparencyAuditCard class="mb-4" />
|
||||
<SingleColorAuditCard class="mb-4" />
|
||||
<TagMaintenanceCard />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MinDimensionCard from '../components/cleanup/MinDimensionCard.vue'
|
||||
import TransparencyAuditCard from '../components/cleanup/TransparencyAuditCard.vue'
|
||||
import SingleColorAuditCard from '../components/cleanup/SingleColorAuditCard.vue'
|
||||
// Reuse existing TagMaintenanceCard (FC-3k) as-is — it already handles
|
||||
// preview + commit of prune-unused-tags via the admin store. Operator
|
||||
// confirmed 2026-05-26: don't duplicate into a new UnusedTagsCard.
|
||||
// MaintenancePanel drops its TagMaintenanceCard reference in the
|
||||
// SettingsView edit (Task 16) so this is now the sole rendering site.
|
||||
import TagMaintenanceCard from '../components/settings/TagMaintenanceCard.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-cleanup { max-width: 900px; }
|
||||
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
</style>
|
||||
@@ -14,6 +14,7 @@
|
||||
<v-tab value="overview">Overview</v-tab>
|
||||
<v-tab value="activity">Activity</v-tab>
|
||||
<v-tab value="import">Import</v-tab>
|
||||
<v-tab value="cleanup">Cleanup</v-tab>
|
||||
<v-tab value="maintenance">Maintenance</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
@@ -53,6 +54,10 @@
|
||||
<ImportFiltersForm />
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="cleanup">
|
||||
<CleanupView />
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="maintenance">
|
||||
<MaintenancePanel />
|
||||
</v-window-item>
|
||||
@@ -72,6 +77,7 @@ import ImportTriggerPanel from '../components/settings/ImportTriggerPanel.vue'
|
||||
import ImportFiltersForm from '../components/settings/ImportFiltersForm.vue'
|
||||
import ImportTaskList from '../components/settings/ImportTaskList.vue'
|
||||
import MaintenancePanel from '../components/settings/MaintenancePanel.vue'
|
||||
import CleanupView from './CleanupView.vue'
|
||||
import { useMLStore } from '../stores/ml.js'
|
||||
|
||||
const tab = ref('overview')
|
||||
|
||||
@@ -150,7 +150,7 @@ async def test_audit_get_by_id_returns_full_row(client, db):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_audit_history_returns_recent_runs(client, db):
|
||||
for i in range(3):
|
||||
for _ in range(3):
|
||||
db.add(LibraryAuditRun(
|
||||
rule="transparency", params={"threshold": 0.9},
|
||||
status="applied", matched_ids=[],
|
||||
|
||||
Reference in New Issue
Block a user