fix: system health is a Settings tab, not a page only the dot reached
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
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
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
This commit is contained in:
@@ -6,7 +6,8 @@
|
||||
<span class="fc-brand__text">FabledCurator</span>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
:to="{ name: 'system' }" class="fc-health" :title="health.label"
|
||||
:to="{ name: 'settings', query: { tab: 'system' } }"
|
||||
class="fc-health" :title="health.label"
|
||||
:aria-label="`System health: ${health.label}`"
|
||||
>
|
||||
<v-icon size="x-small" :color="health.color">{{ health.icon }}</v-icon>
|
||||
@@ -282,9 +283,9 @@ onUnmounted(() => { if (healthTimer) clearInterval(healthTimer) })
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
/* A RouterLink since milestone 365 — it is the path to /system, not just an
|
||||
indicator. Reset the anchor so turning a span into a link changed nothing
|
||||
about how the nav reads. */
|
||||
/* A RouterLink since milestone 365 — it is the path to the Settings System
|
||||
tab, not just an indicator. Reset the anchor so turning a span into a
|
||||
link changed nothing about how the nav reads. */
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-radius: 50%;
|
||||
|
||||
+16
-17
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<v-container class="py-6" style="max-width: 900px">
|
||||
<!-- A Settings tab, not a page of its own (operator 2026-09-02): the first
|
||||
cut hung this off the health dot alone, which is a target you have to
|
||||
already suspect something to look for. Settings is where someone goes
|
||||
to ask the instance about itself, so it lives beside Activity. -->
|
||||
<div>
|
||||
<div class="d-flex align-center mb-1">
|
||||
<h1 class="text-h5">System</h1>
|
||||
<v-spacer />
|
||||
<span class="fc-sys__checked">
|
||||
{{ store.checkedAt ? `checked ${formatRelative(store.checkedAt)}` : 'checking…' }}
|
||||
@@ -62,23 +65,17 @@
|
||||
then neither, and an indicator that reddened on every update would stop
|
||||
being read.
|
||||
</p>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
import { useSystemHealthStore } from '../stores/systemHealth.js'
|
||||
import { formatRelative } from '../utils/date.js'
|
||||
import { useSystemHealthStore } from '../../stores/systemHealth.js'
|
||||
import { formatRelative } from '../../utils/date.js'
|
||||
|
||||
const store = useSystemHealthStore()
|
||||
|
||||
// Slower than the pipeline chip's 8s: liveness changes on the scale of
|
||||
// container restarts, not task starts, and this page is open while someone
|
||||
// watches it.
|
||||
const POLL_MS = 10_000
|
||||
let timer = null
|
||||
|
||||
function kindLabel(kind) {
|
||||
if (kind === 'celery') return 'background worker'
|
||||
if (kind === 'agent') return 'GPU agent'
|
||||
@@ -86,11 +83,13 @@ function kindLabel(kind) {
|
||||
return kind
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
store.refresh()
|
||||
timer = setInterval(() => { if (!document.hidden) store.refresh() }, POLL_MS)
|
||||
})
|
||||
onUnmounted(() => { if (timer) clearInterval(timer) })
|
||||
// No timer of its own. TopNav already polls this same pinia store every 15s
|
||||
// for the health dot, and it is mounted on every route this tab is reachable
|
||||
// from — a second interval here would just double the request rate for a 5s
|
||||
// freshness gain. v-window keeps a visited item MOUNTED (hidden, not
|
||||
// destroyed), so a local timer would also have kept firing behind Maintenance.
|
||||
// One refresh on open, so arriving at the tab doesn't wait out the nav's tick.
|
||||
onMounted(() => { store.refresh() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
|
||||
import SettingsView from './views/SettingsView.vue'
|
||||
import SystemView from './views/SystemView.vue'
|
||||
import GalleryView from './views/GalleryView.vue'
|
||||
import ShowcaseView from './views/ShowcaseView.vue'
|
||||
import ExploreView from './views/ExploreView.vue'
|
||||
@@ -46,12 +45,13 @@ const routes = [
|
||||
|
||||
// Settings — config, pinned to the right of the nav (TopNav special-cases it).
|
||||
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings', stickyChrome: true } },
|
||||
// Deliberately NO meta.title: TopNav builds its nav row from routes that
|
||||
// have one, and this is reached from the health indicator beside the
|
||||
// brand — the place someone already looks when they suspect something is
|
||||
// wrong. A sixth top-level tab for a page you visit twice a year would
|
||||
// cost more attention than it returns.
|
||||
{ path: '/system', name: 'system', component: SystemView },
|
||||
// System health is a Settings TAB, not a route of its own (operator
|
||||
// 2026-09-02). It first shipped as /system reachable only from the health
|
||||
// dot, which is a target you have to already suspect something to go
|
||||
// looking for. Settings is where someone goes to ask the instance about
|
||||
// itself. The path stays as a redirect so the dot's old link, and any
|
||||
// bookmark from that build, still land somewhere real.
|
||||
{ path: '/system', name: 'system', redirect: () => ({ name: 'settings', query: { tab: 'system' } }) },
|
||||
|
||||
// The old standalone paths now redirect into the Browse hub, preserving any
|
||||
// deep-link query (e.g. /posts?post_id=N → /browse?tab=posts&post_id=N). The
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
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>
|
||||
@@ -42,6 +43,13 @@
|
||||
</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>
|
||||
@@ -73,18 +81,24 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
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'
|
||||
|
||||
const tab = ref('overview')
|
||||
// ?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()
|
||||
|
||||
|
||||
@@ -40,6 +40,14 @@ describe('router', () => {
|
||||
expect(router.currentRoute.value.query.post_id).toBe('7')
|
||||
})
|
||||
|
||||
it('/system redirects into the Settings System tab', async () => {
|
||||
// It shipped as a standalone page for one build; the health dot and any
|
||||
// bookmark from it must still land on the surface, which is now a tab.
|
||||
await router.push('/system')
|
||||
expect(router.currentRoute.value.name).toBe('settings')
|
||||
expect(router.currentRoute.value.query.tab).toBe('system')
|
||||
})
|
||||
|
||||
it('series-read is an immersive route', () => {
|
||||
const r = router.resolve('/series/5/read')
|
||||
expect(r.name).toBe('series-read')
|
||||
|
||||
Reference in New Issue
Block a user