98673d4dca
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.
110 lines
3.3 KiB
Vue
110 lines
3.3 KiB
Vue
<template>
|
|
<v-table density="compact" class="fc-queues-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Queue</th>
|
|
<th class="text-right">Depth</th>
|
|
<th class="text-right">Workers</th>
|
|
<th v-if="!compact" class="text-right">Active</th>
|
|
<th class="text-right">Last min: ok / err</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="q in QUEUE_NAMES" :key="q">
|
|
<td>{{ q }}</td>
|
|
<td class="text-right fc-tabular" :class="depthClass(q)">
|
|
{{ formatDepth(q) }}
|
|
</td>
|
|
<td class="text-right fc-tabular">{{ workerCount(q) }}</td>
|
|
<td v-if="!compact" class="text-right fc-tabular">{{ activeCount(q) }}</td>
|
|
<td class="text-right fc-tabular">
|
|
<span class="fc-ok">{{ recentOk(q) }}</span>
|
|
<span class="fc-sep">/</span>
|
|
<span :class="recentErr(q) > 0 ? 'fc-err' : 'fc-muted'">
|
|
{{ recentErr(q) }}<span v-if="recentErr(q) > 0"> ⚠</span>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
|
|
const props = defineProps({
|
|
queues: { type: Object, default: null }, // store.queues
|
|
workers: { type: Object, default: null }, // store.workers
|
|
recentRuns: { type: Array, default: () => [] }, // store.recentRuns
|
|
compact: { type: Boolean, default: false },
|
|
})
|
|
|
|
const QUEUE_NAMES = [
|
|
'default', 'import', 'thumbnail', 'ml',
|
|
'download', 'scan', 'maintenance',
|
|
]
|
|
|
|
function formatDepth(name) {
|
|
const d = props.queues?.queues?.[name]
|
|
if (d == null) return '—'
|
|
return d.toLocaleString()
|
|
}
|
|
|
|
function depthClass(name) {
|
|
const d = props.queues?.queues?.[name]
|
|
const workers = workerCount(name)
|
|
// Queue has depth but no workers → warning.
|
|
if (d != null && d > 0 && workers === 0) return 'fc-warn'
|
|
return ''
|
|
}
|
|
|
|
function workerCount(name) {
|
|
if (!props.workers?.workers) return 0
|
|
let count = 0
|
|
for (const info of Object.values(props.workers.workers)) {
|
|
if (info.queues?.includes(name)) count++
|
|
}
|
|
return count
|
|
}
|
|
|
|
function activeCount(name) {
|
|
if (!props.workers?.workers) return 0
|
|
// Active count is per-worker, not per-queue. Approximate: sum of
|
|
// active_count across workers that subscribe to this queue.
|
|
let count = 0
|
|
for (const info of Object.values(props.workers.workers)) {
|
|
if (info.queues?.includes(name)) count += info.active_count || 0
|
|
}
|
|
return count
|
|
}
|
|
|
|
const recentByQueue = computed(() => {
|
|
const out = {}
|
|
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++
|
|
}
|
|
return out
|
|
})
|
|
|
|
function recentOk(name) { return recentByQueue.value[name]?.ok || 0 }
|
|
function recentErr(name) { return recentByQueue.value[name]?.err || 0 }
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-queues-table { background: transparent; }
|
|
.fc-tabular {
|
|
font-variant-numeric: tabular-nums;
|
|
font-feature-settings: 'tnum';
|
|
}
|
|
.fc-ok { color: rgb(var(--v-theme-success, 76 175 80)); }
|
|
.fc-err { color: rgb(var(--v-theme-error, 220 80 80)); }
|
|
.fc-warn { color: rgb(var(--v-theme-warning, 255 179 0)); font-weight: 500; }
|
|
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
|
.fc-sep {
|
|
margin: 0 4px;
|
|
color: rgb(var(--v-theme-on-surface-variant));
|
|
}
|
|
</style>
|