CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 4s
CI and images / frontend-build (push) Successful in 25s
CI and images / extension-test (push) Successful in 28s
CI and images / backend-lint-and-test (push) Successful in 34s
CI and images / integration (push) Failing after 2m24s
CI and images / sign-extension (push) Skipped
CI and images / build-web (push) Skipped
CI and images / smoke-web (push) Skipped
CI and images / promote (push) Skipped
CI and images / build-agent (push) Skipped
celery_signals._queue_for was a hand-kept copy of task_routes and had drifted: - backup, admin and library_audit jobs, and backfill_phash, run on maintenance_long but were recorded as `maintenance`; - translation and gpu_queue jobs were recorded as `default`, where the 5-minute stall sweep failed healthy 35-minute translation runs. It now asks the router, cached per task name. A new guard test checks every registered task's hard time limit against the stall threshold the sweep would use for it. It also caught these sweeps, which failed healthy runs mid-flight and are fixed here: - ml's scheduled sweeps (35 min, previously swept at 25); - train_heads and apply_head_tags (65 min); - import_media_file (6 min, previously swept at 5); - the long lane, which now has its own 45-minute threshold. UI changes: - the admin job poller follows a job by celery_task_id, via a new filter on /runs, instead of by lane; - the archive re-extract and missing-file repair cards show the long lane's backlog, where their jobs actually wait; - the queue table lists maintenance_long. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
109 lines
3.2 KiB
Vue
109 lines
3.2 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', 'maintenance_long',
|
|
]
|
|
|
|
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-sep {
|
|
margin: 0 4px;
|
|
color: rgb(var(--v-theme-on-surface-variant));
|
|
}
|
|
</style>
|