From 98673d4dcaf40d62c5e9227aaed03c7591709d78 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 2 Jun 2026 16:46:46 -0400 Subject: [PATCH] fix(audit-g5a): small architectural cleanups bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five small G5 findings from the 2026-06-02 audit. Each is local and follows an established FC pattern. - download_service: replace hardcoded ('discord','pixiv') tuple with auth_type_for(platform) == 'token'. A 7th token-platform now picks up the right credential path without touching this site. - /api/tags//merge enqueues recompute_centroid.delay after merge so the target's centroid reflects its new image set immediately. Daily list_drifted catches it within 24h, but eager recompute closes the suggestion-quality dip in the meantime. - backfill_thumbnails added to beat_schedule (daily). The task docstring claimed periodic Beat but the entry was never registered, so the library got no self-healing thumbnail repair; only the manual admin-UI button fired it. - modal.createAndAdd pushes a kind='fandom' tag into tagsStore.fandomCache so FandomPicker sees the new fandom on next open. Was: cache-gated load (length===0) skipped refetch, new fandom invisible until full page reload. - cleanup cluster: - Drop .webp from cleanup_service.unlink — thumbnailer only writes .jpg/.png; the third tuple member was dead code. - Drop effective_date from /api/gallery/scroll response — no FE consumer reads it. Service still computes the attribute for timeline ordering; this just trims the JSON. - Rename store.recentMinute → store.recentRuns across the systemActivity store + three consumers (SystemActivitySummary, QueuesTable, SystemActivityTab). The data is the last 200 runs (not actually "last minute"), so the name lied. NOT in this bundle: the duplicate tag-merge endpoint (/api/tags vs /api/admin/tags) is harder — has 1 FE caller and 3 tests on the admin variant; consolidation is its own change. --- backend/app/api/gallery.py | 1 - backend/app/api/tags.py | 6 ++++++ backend/app/celery_app.py | 10 ++++++++++ backend/app/services/cleanup_service.py | 8 ++++++-- backend/app/services/download_service.py | 8 +++++++- frontend/src/components/settings/QueuesTable.vue | 4 ++-- .../components/settings/SystemActivitySummary.vue | 4 ++-- .../src/components/settings/SystemActivityTab.vue | 4 ++-- frontend/src/stores/modal.js | 13 +++++++++++++ frontend/src/stores/systemActivity.js | 10 +++++----- 10 files changed, 53 insertions(+), 15 deletions(-) diff --git a/backend/app/api/gallery.py b/backend/app/api/gallery.py index 8de2ed8..41b561c 100644 --- a/backend/app/api/gallery.py +++ b/backend/app/api/gallery.py @@ -43,7 +43,6 @@ async def scroll(): "height": i.height, "created_at": i.created_at.isoformat(), "posted_at": i.posted_at.isoformat() if i.posted_at else None, - "effective_date": i.effective_date.isoformat(), "thumbnail_url": i.thumbnail_url, "artist": i.artist, } diff --git a/backend/app/api/tags.py b/backend/app/api/tags.py index e18d94d..9061985 100644 --- a/backend/app/api/tags.py +++ b/backend/app/api/tags.py @@ -245,6 +245,12 @@ async def merge_tag(source_id: int): from ..tasks.ml import apply_allowlist_tags apply_allowlist_tags.delay(tag_id=result.target_id) + # Tag merge invalidates the target's centroid (the merged-in source + # tag's images now contribute to it). Daily list_drifted catches it + # within 24h, but eager recompute closes the suggestion-quality dip + # in the meantime. Audit 2026-06-02. + from ..tasks.ml import recompute_centroid + recompute_centroid.delay(result.target_id) return jsonify( { "target": { diff --git a/backend/app/celery_app.py b/backend/app/celery_app.py index 4051e21..eca48e8 100644 --- a/backend/app/celery_app.py +++ b/backend/app/celery_app.py @@ -130,6 +130,16 @@ def make_celery() -> Celery: "task": "backend.app.tasks.maintenance.prune_import_batches", "schedule": 86400.0, }, + # Audit 2026-06-02 — backfill_thumbnails's docstring claimed + # "periodic Beat" but the entry was never registered, so the + # library got no self-healing thumbnail repair; only the + # manual admin-UI button fired it. Daily cadence is gentle + # (the task is idempotent and only enqueues regen for rows + # whose stored thumbnails are missing or corrupt). + "backfill-thumbnails-daily": { + "task": "backend.app.tasks.thumbnail.backfill_thumbnails", + "schedule": 86400.0, + }, }, timezone="UTC", ) diff --git a/backend/app/services/cleanup_service.py b/backend/app/services/cleanup_service.py index 28b20da..58aab98 100644 --- a/backend/app/services/cleanup_service.py +++ b/backend/app/services/cleanup_service.py @@ -187,10 +187,14 @@ def unlink_image_files( out["thumbnail"] = True except OSError: out["thumbnail"] = False - # Convention thumbs dir — try all extensions; missing OK. + # Convention thumbs dir — try both extensions thumbnailer writes + # (.jpg for opaque, .png for alpha). `.webp` used to be in this + # tuple but the thumbnailer never writes it (operator-flagged in + # the 2026-06-02 audit) — keep the tuple aligned with what + # actually lands on disk. if image.sha256: bucket = image.sha256[:3] - for ext in ("jpg", "png", "webp"): + for ext in ("jpg", "png"): try: (images_root / "thumbs" / bucket / f"{image.sha256}.{ext}").unlink( missing_ok=True, diff --git a/backend/app/services/download_service.py b/backend/app/services/download_service.py index d8c15e9..bc2b361 100644 --- a/backend/app/services/download_service.py +++ b/backend/app/services/download_service.py @@ -34,6 +34,7 @@ from .gallery_dl import ( SourceConfig, ) from .importer import Importer +from .platforms import auth_type_for from .patreon_resolver import resolve_campaign_id from .scheduler_service import set_platform_cooldown @@ -194,7 +195,12 @@ class DownloadService: event_id = ev.id artist = source.artist - if source.platform in ("discord", "pixiv"): + # Drive cookies-vs-token selection from the platform registry's + # auth_type so a new 7th token-platform automatically picks the + # right credential path. The hardcoded tuple here used to drift + # out of sync with credential_service's auth_type_for(). Audit + # 2026-06-02. + if auth_type_for(source.platform) == "token": cookies_path = None auth_token = await self.cred_service.get_token(source.platform) else: diff --git a/frontend/src/components/settings/QueuesTable.vue b/frontend/src/components/settings/QueuesTable.vue index 2edcfb2..b326101 100644 --- a/frontend/src/components/settings/QueuesTable.vue +++ b/frontend/src/components/settings/QueuesTable.vue @@ -35,7 +35,7 @@ import { computed } from 'vue' const props = defineProps({ queues: { type: Object, default: null }, // store.queues workers: { type: Object, default: null }, // store.workers - recentMinute: { type: Array, default: () => [] }, // store.recentMinute + recentRuns: { type: Array, default: () => [] }, // store.recentRuns compact: { type: Boolean, default: false }, }) @@ -80,7 +80,7 @@ function activeCount(name) { const recentByQueue = computed(() => { const out = {} - for (const r of props.recentMinute) { + for (const r of props.recentRuns) { if (!out[r.queue]) out[r.queue] = { ok: 0, err: 0 } if (r.status === 'ok') out[r.queue].ok++ else if (r.status === 'error' || r.status === 'timeout') out[r.queue].err++ diff --git a/frontend/src/components/settings/SystemActivitySummary.vue b/frontend/src/components/settings/SystemActivitySummary.vue index c494bbd..354f94f 100644 --- a/frontend/src/components/settings/SystemActivitySummary.vue +++ b/frontend/src/components/settings/SystemActivitySummary.vue @@ -24,7 +24,7 @@ @@ -47,7 +47,7 @@ function pollOnce() { if (document.hidden) return store.loadQueues() store.loadWorkers() - store.loadRecentMinute() + store.loadRecentRuns() } onMounted(() => { diff --git a/frontend/src/components/settings/SystemActivityTab.vue b/frontend/src/components/settings/SystemActivityTab.vue index fe9db71..ff2f7a7 100644 --- a/frontend/src/components/settings/SystemActivityTab.vue +++ b/frontend/src/components/settings/SystemActivityTab.vue @@ -14,7 +14,7 @@ @@ -202,7 +202,7 @@ function pollQueues() { if (document.hidden) return store.loadQueues() store.loadWorkers() - store.loadRecentMinute() + store.loadRecentRuns() } function pollFailures() { if (document.hidden) return diff --git a/frontend/src/stores/modal.js b/frontend/src/stores/modal.js index 2b89d76..05876d0 100644 --- a/frontend/src/stores/modal.js +++ b/frontend/src/stores/modal.js @@ -155,6 +155,19 @@ export const useModalStore = defineStore('modal', () => { const imageId = currentImageId.value if (!imageId) return const tag = await api.post('/api/tags', { body: { name, kind, fandom_id } }) + // Audit 2026-06-02: a kind='fandom' created here used to be + // invisible to FandomPicker until a full page reload — its load + // gates on fandomCache.length, so a non-empty cache skips the + // refetch and the new fandom never appears. Push it into the + // cache directly so the next open sees it. + if (kind === 'fandom') { + const { useTagStore } = await import('./tags.js') + const tagStore = useTagStore() + tagStore.fandomCache.push({ + id: tag.id, name: tag.name, kind: 'fandom', + fandom_id: null, fandom_name: null, image_count: 0, + }) + } if (currentImageId.value !== imageId) return // navigated away await addExistingTag(tag.id) } diff --git a/frontend/src/stores/systemActivity.js b/frontend/src/stores/systemActivity.js index 7d8252f..44980a6 100644 --- a/frontend/src/stores/systemActivity.js +++ b/frontend/src/stores/systemActivity.js @@ -9,7 +9,7 @@ export const useSystemActivityStore = defineStore('systemActivity', () => { // Live polled state. const queues = ref(null) // { queues: {name: depth|null}, fetched_at } const workers = ref(null) // { workers: {hostname: {...}}, fetched_at } - const recentMinute = ref([]) // last-60s rows (for Overview summary) + const recentRuns = ref([]) // last-60s rows (for Overview summary) const failures = ref(null) // { recent, count_by_type, since } // Paginated runs (Activity tab "All recent activity" pane). @@ -45,14 +45,14 @@ export const useSystemActivityStore = defineStore('systemActivity', () => { } } - async function loadRecentMinute() { + async function loadRecentRuns() { // Used by the Overview summary card: pull last 60s of runs to compute // per-queue ok/err counts. One call covers all queues; UI groups. try { const body = await api.get('/api/system/activity/runs', { params: { limit: 200 }, }) - recentMinute.value = body.runs || [] + recentRuns.value = body.runs || [] } catch (e) { lastError.value = e.message } @@ -106,10 +106,10 @@ export const useSystemActivityStore = defineStore('systemActivity', () => { } return { - queues, workers, recentMinute, failures, summary, + queues, workers, recentRuns, failures, summary, runs, runsCursor, runsHasMore, runsFilter, loading, lastError, - loadQueues, loadWorkers, loadRecentMinute, + loadQueues, loadWorkers, loadRecentRuns, loadRuns, loadFailures, loadSummary, setFilter, } })