9b252948f9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
|
|
recentMinute: { type: Array, default: () => [] }, // store.recentMinute
|
|
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.recentMinute) {
|
|
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>
|