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,
}
})