fix(audit-g5a): small architectural cleanups bundle
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/<source_id>/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.
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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++
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<QueuesTable
|
||||
:queues="store.queues"
|
||||
:workers="store.workers"
|
||||
:recent-minute="store.recentMinute"
|
||||
:recent-runs="store.recentRuns"
|
||||
compact
|
||||
/>
|
||||
</v-card-text>
|
||||
@@ -47,7 +47,7 @@ function pollOnce() {
|
||||
if (document.hidden) return
|
||||
store.loadQueues()
|
||||
store.loadWorkers()
|
||||
store.loadRecentMinute()
|
||||
store.loadRecentRuns()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<QueuesTable
|
||||
:queues="store.queues"
|
||||
:workers="store.workers"
|
||||
:recent-minute="store.recentMinute"
|
||||
:recent-runs="store.recentRuns"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -202,7 +202,7 @@ function pollQueues() {
|
||||
if (document.hidden) return
|
||||
store.loadQueues()
|
||||
store.loadWorkers()
|
||||
store.loadRecentMinute()
|
||||
store.loadRecentRuns()
|
||||
}
|
||||
function pollFailures() {
|
||||
if (document.hidden) return
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user