Admin IA overhaul, cap-aware autoscaler, optional ml-worker, hardened tag reset #187
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<v-card class="mb-4">
|
||||||
|
<CardHeading icon="mdi-download" title="Downloads (last 24h)">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
variant="text" size="small" rounded="pill"
|
||||||
|
to="/subscriptions?tab=downloads"
|
||||||
|
>
|
||||||
|
Open subscriptions
|
||||||
|
<v-icon end size="small">mdi-arrow-right</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</CardHeading>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex align-center flex-wrap mb-2" style="gap: 8px">
|
||||||
|
<v-chip size="small" variant="tonal" color="success">
|
||||||
|
{{ stats.ok }} ok
|
||||||
|
</v-chip>
|
||||||
|
<v-chip
|
||||||
|
size="small" variant="tonal"
|
||||||
|
:color="stats.error ? 'error' : undefined"
|
||||||
|
>
|
||||||
|
{{ stats.error }} failed
|
||||||
|
</v-chip>
|
||||||
|
<v-chip v-if="stats.running" size="small" variant="tonal" color="accent">
|
||||||
|
{{ stats.running }} running
|
||||||
|
</v-chip>
|
||||||
|
<v-chip v-if="stats.pending" size="small" variant="tonal">
|
||||||
|
{{ stats.pending }} pending
|
||||||
|
</v-chip>
|
||||||
|
<v-chip v-if="stats.skipped" size="small" variant="tonal">
|
||||||
|
{{ stats.skipped }} skipped
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
<p v-if="!failing.length" class="fc-muted text-body-2 mb-0">
|
||||||
|
All subscription sources healthy.
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-body-2 mb-0">
|
||||||
|
<b class="fc-bad">{{ failing.length }}</b> failing source(s):
|
||||||
|
<span class="fc-muted">{{ failingNames }}</span>
|
||||||
|
</p>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
import CardHeading from '../common/CardHeading.vue'
|
||||||
|
import { useDownloadsStore } from '../../stores/downloads.js'
|
||||||
|
|
||||||
|
const store = useDownloadsStore()
|
||||||
|
const { stats, failing } = storeToRefs(store)
|
||||||
|
let pollId = null
|
||||||
|
|
||||||
|
const failingNames = computed(() => {
|
||||||
|
const names = failing.value.map((s) => s.artist_name || s.url).slice(0, 3)
|
||||||
|
const extra = failing.value.length - names.length
|
||||||
|
return names.join(', ') + (extra > 0 ? ` +${extra} more` : '')
|
||||||
|
})
|
||||||
|
|
||||||
|
function poll() {
|
||||||
|
store.loadStats(24)
|
||||||
|
store.loadFailing()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
poll()
|
||||||
|
pollId = setInterval(() => { if (!document.hidden) poll() }, 30000)
|
||||||
|
})
|
||||||
|
onUnmounted(() => { if (pollId) clearInterval(pollId) })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||||
|
.fc-bad { color: rgb(var(--v-theme-error)); }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<v-card class="mb-4">
|
||||||
|
<CardHeading icon="mdi-expansion-card" title="GPU agent pipeline">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
variant="text" size="small" rounded="pill"
|
||||||
|
@click="$emit('open-maintenance')"
|
||||||
|
>
|
||||||
|
Open maintenance
|
||||||
|
<v-icon end size="small">mdi-arrow-right</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</CardHeading>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="fc-cells mb-2">
|
||||||
|
<div class="fc-cell">
|
||||||
|
<div class="fc-cell__n">{{ q.pending }}</div>
|
||||||
|
<div class="fc-cell__l">pending</div>
|
||||||
|
</div>
|
||||||
|
<div class="fc-cell">
|
||||||
|
<div class="fc-cell__n">{{ q.leased }}</div>
|
||||||
|
<div class="fc-cell__l">in flight</div>
|
||||||
|
</div>
|
||||||
|
<div class="fc-cell">
|
||||||
|
<div class="fc-cell__n fc-good">{{ q.done }}</div>
|
||||||
|
<div class="fc-cell__l">done</div>
|
||||||
|
</div>
|
||||||
|
<div class="fc-cell">
|
||||||
|
<div class="fc-cell__n" :class="q.error ? 'fc-bad' : ''">{{ q.error }}</div>
|
||||||
|
<div class="fc-cell__l">errored</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="!q.error" class="fc-muted text-body-2 mb-0">
|
||||||
|
No failed jobs — the pipeline is clean. Work drains whenever the
|
||||||
|
desktop agent is running.
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-body-2 mb-0">
|
||||||
|
Triage: <b>{{ triage.defect }}</b> defective file(s) ·
|
||||||
|
{{ triage.file_ok }} file-ok · {{ triage.unclassified }} unprobed
|
||||||
|
<span v-if="reasonSummary" class="fc-muted">— {{ reasonSummary }}</span>
|
||||||
|
<br>
|
||||||
|
<span class="fc-muted text-caption">
|
||||||
|
Recover defective files from Maintenance → Failed processing.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import CardHeading from '../common/CardHeading.vue'
|
||||||
|
import { useGpuStore } from '../../stores/gpu.js'
|
||||||
|
|
||||||
|
defineEmits(['open-maintenance'])
|
||||||
|
|
||||||
|
const store = useGpuStore()
|
||||||
|
const q = ref({ pending: 0, leased: 0, done: 0, error: 0 })
|
||||||
|
const triage = ref({ defect: 0, file_ok: 0, unclassified: 0 })
|
||||||
|
const byClass = ref({})
|
||||||
|
let pollId = null
|
||||||
|
let lastErrorCount = -1
|
||||||
|
|
||||||
|
const reasonSummary = computed(() =>
|
||||||
|
Object.entries(byClass.value)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 3)
|
||||||
|
.map(([k, n]) => `${k.replaceAll('_', ' ')} ${n}`)
|
||||||
|
.join(' · '))
|
||||||
|
|
||||||
|
async function poll() {
|
||||||
|
try {
|
||||||
|
q.value = await store.status()
|
||||||
|
// The triage detail is only worth a second call when the error count
|
||||||
|
// actually moved (it's a 500-row join server-side).
|
||||||
|
if (q.value.error !== lastErrorCount) {
|
||||||
|
lastErrorCount = q.value.error
|
||||||
|
if (q.value.error > 0) {
|
||||||
|
const body = await store.errors()
|
||||||
|
triage.value = body.triage
|
||||||
|
byClass.value = body.by_class
|
||||||
|
} else {
|
||||||
|
triage.value = { defect: 0, file_ok: 0, unclassified: 0 }
|
||||||
|
byClass.value = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* non-fatal — panel just shows the last snapshot */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
poll()
|
||||||
|
pollId = setInterval(() => { if (!document.hidden) poll() }, 5000)
|
||||||
|
})
|
||||||
|
onUnmounted(() => { if (pollId) clearInterval(pollId) })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||||
|
.fc-cells { display: flex; gap: 28px; }
|
||||||
|
.fc-cell__n {
|
||||||
|
font-size: 20px; font-weight: 700; line-height: 1.1;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
.fc-cell__l {
|
||||||
|
font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
|
color: rgb(var(--v-theme-on-surface-variant));
|
||||||
|
}
|
||||||
|
.fc-good { color: rgb(var(--v-theme-success)); }
|
||||||
|
.fc-bad { color: rgb(var(--v-theme-error)); }
|
||||||
|
</style>
|
||||||
@@ -17,6 +17,12 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- The non-Celery halves of the app (2026-07-02): the GPU agent does the
|
||||||
|
majority of processing and downloads feed the library — Activity is
|
||||||
|
the whole-app pulse, not just the worker queues. -->
|
||||||
|
<GpuActivityPanel @open-maintenance="$emit('open-maintenance')" />
|
||||||
|
<DownloadsActivityPanel />
|
||||||
|
|
||||||
<!-- Recent failures pane -->
|
<!-- Recent failures pane -->
|
||||||
<v-card class="mb-4">
|
<v-card class="mb-4">
|
||||||
<CardHeading icon="mdi-alert-circle-outline" title="Recent failures (last 24h)">
|
<CardHeading icon="mdi-alert-circle-outline" title="Recent failures (last 24h)">
|
||||||
@@ -167,6 +173,10 @@ import { formatRelative as fmtRelative } from '../../utils/date.js'
|
|||||||
import ErrorDetailModal from '../common/ErrorDetailModal.vue'
|
import ErrorDetailModal from '../common/ErrorDetailModal.vue'
|
||||||
import QueuesTable from './QueuesTable.vue'
|
import QueuesTable from './QueuesTable.vue'
|
||||||
import CardHeading from '../common/CardHeading.vue'
|
import CardHeading from '../common/CardHeading.vue'
|
||||||
|
import GpuActivityPanel from './GpuActivityPanel.vue'
|
||||||
|
import DownloadsActivityPanel from './DownloadsActivityPanel.vue'
|
||||||
|
|
||||||
|
defineEmits(['open-maintenance'])
|
||||||
|
|
||||||
// Click-to-open modal for full error text. Replaces the unusable
|
// Click-to-open modal for full error text. Replaces the unusable
|
||||||
// :title="..." tooltip (operator-flagged 2026-05-26: SQLAlchemy
|
// :title="..." tooltip (operator-flagged 2026-05-26: SQLAlchemy
|
||||||
|
|||||||
@@ -28,6 +28,13 @@
|
|||||||
class="mt-4"
|
class="mt-4"
|
||||||
@open-activity="tab = 'activity'"
|
@open-activity="tab = 'activity'"
|
||||||
/>
|
/>
|
||||||
|
<!-- Health strip (2026-07-02): the same GPU + downloads panels the
|
||||||
|
Activity tab uses — Overview answers "is everything healthy?"
|
||||||
|
across ALL systems, not just the Celery queues above. -->
|
||||||
|
<div class="fc-health-row mt-4">
|
||||||
|
<GpuActivityPanel @open-maintenance="tab = 'maintenance'" />
|
||||||
|
<DownloadsActivityPanel />
|
||||||
|
</div>
|
||||||
<v-alert
|
<v-alert
|
||||||
v-if="system.stats && (system.stats.tasks.pending + system.stats.tasks.queued) > 0"
|
v-if="system.stats && (system.stats.tasks.pending + system.stats.tasks.queued) > 0"
|
||||||
type="info" variant="tonal" class="mt-4" closable
|
type="info" variant="tonal" class="mt-4" closable
|
||||||
@@ -38,7 +45,7 @@
|
|||||||
</v-window-item>
|
</v-window-item>
|
||||||
|
|
||||||
<v-window-item value="activity">
|
<v-window-item value="activity">
|
||||||
<SystemActivityTab />
|
<SystemActivityTab @open-maintenance="tab = 'maintenance'" />
|
||||||
</v-window-item>
|
</v-window-item>
|
||||||
|
|
||||||
<v-window-item value="cleanup">
|
<v-window-item value="cleanup">
|
||||||
@@ -58,6 +65,8 @@ import { useSystemStore } from '../stores/system.js'
|
|||||||
import SystemStatsCards from '../components/settings/SystemStatsCards.vue'
|
import SystemStatsCards from '../components/settings/SystemStatsCards.vue'
|
||||||
import SystemActivitySummary from '../components/settings/SystemActivitySummary.vue'
|
import SystemActivitySummary from '../components/settings/SystemActivitySummary.vue'
|
||||||
import SystemActivityTab from '../components/settings/SystemActivityTab.vue'
|
import SystemActivityTab from '../components/settings/SystemActivityTab.vue'
|
||||||
|
import GpuActivityPanel from '../components/settings/GpuActivityPanel.vue'
|
||||||
|
import DownloadsActivityPanel from '../components/settings/DownloadsActivityPanel.vue'
|
||||||
import MaintenancePanel from '../components/settings/MaintenancePanel.vue'
|
import MaintenancePanel from '../components/settings/MaintenancePanel.vue'
|
||||||
import CleanupView from './CleanupView.vue'
|
import CleanupView from './CleanupView.vue'
|
||||||
import { useMLStore } from '../stores/ml.js'
|
import { useMLStore } from '../stores/ml.js'
|
||||||
@@ -93,4 +102,13 @@ watch(tab, (t) => {
|
|||||||
.fc-settings {
|
.fc-settings {
|
||||||
max-width: 1140px;
|
max-width: 1140px;
|
||||||
}
|
}
|
||||||
|
/* Overview health strip: the two pulse panels side by side, stacking on
|
||||||
|
narrow screens. Cancel the panels' own bottom margin — the row owns gaps. */
|
||||||
|
.fc-health-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.fc-health-row :deep(.v-card) { margin-bottom: 0 !important; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user