Files
FabledCurator/frontend/src/views/SettingsView.vue
T
bvandeusenandClaude Opus 5 ad8392b790
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 5s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 8s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 35s
Build images / build-web (push) Successful in 54s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 1m47s
fix: system health is a Settings tab, not a page only the dot reached
The surface shipped at /system with no nav entry, reachable only by
clicking the health dot beside the brand — a target you have to already
suspect something is wrong to go looking for. Operator-flagged: it needs
a path someone can walk to.

Settings is where you go to ask the instance about itself, so the view
becomes a tab there, beside Activity — Activity answers "what is the
queue doing", System answers "is anything left to do it".

- SystemView.vue moves to components/settings/SystemHealthTab.vue; the
  content is unchanged apart from shedding its own container and h1.
- SettingsView adopts useTabQuery (the composable Browse and
  Subscriptions already use) so a tab can be linked TO. The health dot
  now points at ?tab=system, and /system redirects there so the previous
  build's link and any bookmark still land.
- The tab drops its own 10s poll. v-window keeps a visited item mounted
  rather than destroyed, so that timer would have gone on firing behind
  Maintenance — and TopNav already polls the same store every 15s for
  the dot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TTjbZZ6JirCMSaJzQV1RhA
2026-09-02 19:45:54 -04:00

142 lines
5.7 KiB
Vue

<template>
<!-- Centered, width-capped column so the tab strip + every panel sit in a
tidy centered measure rather than stretching full-width (operator
2026-06-18). v-container is margin-auto by default; the cap keeps long
maintenance/import panels readable. -->
<v-container class="pt-2 pb-6 fc-settings">
<!-- Sticky tabs: operator-flagged 2026-05-25 long Import / Maintenance
panels pushed the tab strip out of the viewport, forcing a scroll-
to-top just to change tab. AppShell's TopNav is 64px sticky, so the
tab strip lives directly under it. The .fc-chrome-continues fade
continues the nav's gradient across the strip (operator 2026-07-13). -->
<v-tabs
v-model="tab" color="accent" class="mb-4 fc-chrome-continues"
style="position: sticky; top: var(--fc-nav-h, 64px); z-index: 4;"
>
<v-tab value="overview">Overview</v-tab>
<v-tab value="system">System</v-tab>
<v-tab value="activity">Activity</v-tab>
<v-tab value="cleanup">Cleanup</v-tab>
<v-tab value="maintenance">Maintenance</v-tab>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="overview">
<SystemStatsCards :stats="system.stats" />
<SystemActivitySummary
class="mt-4"
@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-if="system.stats && (system.stats.tasks.pending + system.stats.tasks.queued) > 0"
type="info" variant="tonal" class="mt-4" closable
>
{{ system.stats.tasks.pending + system.stats.tasks.queued }} import task(s) pending.
<v-btn variant="text" size="small" @click="tab = 'activity'">View in Activity</v-btn>
</v-alert>
</v-window-item>
<!-- Is every part of the stack still running (milestone 365). Sits
beside Activity deliberately: Activity answers "what is the queue
doing", this answers "is anything left to do it". -->
<v-window-item value="system">
<SystemHealthTab />
</v-window-item>
<v-window-item value="activity">
<SystemActivityTab @open-maintenance="tab = 'maintenance'" />
</v-window-item>
<v-window-item value="cleanup">
<CleanupView />
</v-window-item>
<v-window-item value="maintenance">
<MaintenancePanel />
</v-window-item>
</v-window>
<!-- Which build is this? With no version image tags (milestone 318) the
instance's own report is the only answer, so it is shown rather than
hidden. The instinct to treat it as information disclosure does not
survive contact: the JS bundle and asset hashes fingerprint the build
anyway, and "I'm on 2026.08.28.1249" is the single most useful line in
a bug report.
Channel sits BESIDE the version, never inside it (rule 149) — a
`-dev` suffix would read as a 0 segment to the extension's comparator
and make every dev build compare equal (#2993). -->
<div v-if="system.buildLoaded" class="text-caption text-medium-emphasis text-center mt-8">
FabledCurator {{ system.buildVersion || 'unknown' }}
<span v-if="system.buildChannel"> · {{ system.buildChannel }}</span>
</div>
</v-container>
</template>
<script setup>
import { onMounted, onUnmounted, watch } from 'vue'
import { useSystemStore } from '../stores/system.js'
import SystemStatsCards from '../components/settings/SystemStatsCards.vue'
import SystemActivitySummary from '../components/settings/SystemActivitySummary.vue'
import SystemActivityTab from '../components/settings/SystemActivityTab.vue'
import SystemHealthTab from '../components/settings/SystemHealthTab.vue'
import GpuActivityPanel from '../components/settings/GpuActivityPanel.vue'
import DownloadsActivityPanel from '../components/settings/DownloadsActivityPanel.vue'
import MaintenancePanel from '../components/settings/MaintenancePanel.vue'
import CleanupView from './CleanupView.vue'
import { useTabQuery } from '../composables/useTabQuery.js'
import { useMLStore } from '../stores/ml.js'
// ?tab= sync (the same composable Browse/Subscriptions use) so a tab can be
// linked TO — the health dot beside the brand points at ?tab=system, and the
// old /system path redirects there.
const VALID_TABS = ['overview', 'system', 'activity', 'cleanup', 'maintenance']
const { tab } = useTabQuery(VALID_TABS, 'overview')
const system = useSystemStore()
const mlStore = useMLStore()
let pollId = null
function startPolling() {
if (pollId) return
system.refreshStats()
pollId = setInterval(() => {
if (!document.hidden) system.refreshStats()
}, 5000)
}
function stopPolling() {
if (pollId) { clearInterval(pollId); pollId = null }
}
onMounted(startPolling)
onUnmounted(stopPolling)
watch(tab, (t) => {
if (t === 'maintenance') mlStore.loadSettings()
})
</script>
<style scoped>
/* Centered, capped measure for the whole Settings surface (tabs + panels). */
.fc-settings {
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>