Compare commits
1
Commits
a8fdbd86bc
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad8392b790 |
@@ -6,7 +6,8 @@
|
|||||||
<span class="fc-brand__text">FabledCurator</span>
|
<span class="fc-brand__text">FabledCurator</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<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}`"
|
:aria-label="`System health: ${health.label}`"
|
||||||
>
|
>
|
||||||
<v-icon size="x-small" :color="health.color">{{ health.icon }}</v-icon>
|
<v-icon size="x-small" :color="health.color">{{ health.icon }}</v-icon>
|
||||||
@@ -282,9 +283,9 @@ onUnmounted(() => { if (healthTimer) clearInterval(healthTimer) })
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
/* A RouterLink since milestone 365 — it is the path to /system, not just an
|
/* A RouterLink since milestone 365 — it is the path to the Settings System
|
||||||
indicator. Reset the anchor so turning a span into a link changed nothing
|
tab, not just an indicator. Reset the anchor so turning a span into a
|
||||||
about how the nav reads. */
|
link changed nothing about how the nav reads. */
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
+16
-17
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<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">
|
<div class="d-flex align-center mb-1">
|
||||||
<h1 class="text-h5">System</h1>
|
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<span class="fc-sys__checked">
|
<span class="fc-sys__checked">
|
||||||
{{ store.checkedAt ? `checked ${formatRelative(store.checkedAt)}` : 'checking…' }}
|
{{ store.checkedAt ? `checked ${formatRelative(store.checkedAt)}` : 'checking…' }}
|
||||||
@@ -62,23 +65,17 @@
|
|||||||
then neither, and an indicator that reddened on every update would stop
|
then neither, and an indicator that reddened on every update would stop
|
||||||
being read.
|
being read.
|
||||||
</p>
|
</p>
|
||||||
</v-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
import { useSystemHealthStore } from '../stores/systemHealth.js'
|
import { useSystemHealthStore } from '../../stores/systemHealth.js'
|
||||||
import { formatRelative } from '../utils/date.js'
|
import { formatRelative } from '../../utils/date.js'
|
||||||
|
|
||||||
const store = useSystemHealthStore()
|
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) {
|
function kindLabel(kind) {
|
||||||
if (kind === 'celery') return 'background worker'
|
if (kind === 'celery') return 'background worker'
|
||||||
if (kind === 'agent') return 'GPU agent'
|
if (kind === 'agent') return 'GPU agent'
|
||||||
@@ -86,11 +83,13 @@ function kindLabel(kind) {
|
|||||||
return kind
|
return kind
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// No timer of its own. TopNav already polls this same pinia store every 15s
|
||||||
store.refresh()
|
// for the health dot, and it is mounted on every route this tab is reachable
|
||||||
timer = setInterval(() => { if (!document.hidden) store.refresh() }, POLL_MS)
|
// 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
|
||||||
onUnmounted(() => { if (timer) clearInterval(timer) })
|
// 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
|
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
|
||||||
import SettingsView from './views/SettingsView.vue'
|
import SettingsView from './views/SettingsView.vue'
|
||||||
import SystemView from './views/SystemView.vue'
|
|
||||||
import GalleryView from './views/GalleryView.vue'
|
import GalleryView from './views/GalleryView.vue'
|
||||||
import ShowcaseView from './views/ShowcaseView.vue'
|
import ShowcaseView from './views/ShowcaseView.vue'
|
||||||
import ExploreView from './views/ExploreView.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).
|
// Settings — config, pinned to the right of the nav (TopNav special-cases it).
|
||||||
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings', stickyChrome: true } },
|
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings', stickyChrome: true } },
|
||||||
// Deliberately NO meta.title: TopNav builds its nav row from routes that
|
// System health is a Settings TAB, not a route of its own (operator
|
||||||
// have one, and this is reached from the health indicator beside the
|
// 2026-09-02). It first shipped as /system reachable only from the health
|
||||||
// brand — the place someone already looks when they suspect something is
|
// dot, which is a target you have to already suspect something to go
|
||||||
// wrong. A sixth top-level tab for a page you visit twice a year would
|
// looking for. Settings is where someone goes to ask the instance about
|
||||||
// cost more attention than it returns.
|
// itself. The path stays as a redirect so the dot's old link, and any
|
||||||
{ path: '/system', name: 'system', component: SystemView },
|
// 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
|
// 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
|
// 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;"
|
style="position: sticky; top: var(--fc-nav-h, 64px); z-index: 4;"
|
||||||
>
|
>
|
||||||
<v-tab value="overview">Overview</v-tab>
|
<v-tab value="overview">Overview</v-tab>
|
||||||
|
<v-tab value="system">System</v-tab>
|
||||||
<v-tab value="activity">Activity</v-tab>
|
<v-tab value="activity">Activity</v-tab>
|
||||||
<v-tab value="cleanup">Cleanup</v-tab>
|
<v-tab value="cleanup">Cleanup</v-tab>
|
||||||
<v-tab value="maintenance">Maintenance</v-tab>
|
<v-tab value="maintenance">Maintenance</v-tab>
|
||||||
@@ -42,6 +43,13 @@
|
|||||||
</v-alert>
|
</v-alert>
|
||||||
</v-window-item>
|
</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">
|
<v-window-item value="activity">
|
||||||
<SystemActivityTab @open-maintenance="tab = 'maintenance'" />
|
<SystemActivityTab @open-maintenance="tab = 'maintenance'" />
|
||||||
</v-window-item>
|
</v-window-item>
|
||||||
@@ -73,18 +81,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useSystemStore } from '../stores/system.js'
|
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 SystemHealthTab from '../components/settings/SystemHealthTab.vue'
|
||||||
import GpuActivityPanel from '../components/settings/GpuActivityPanel.vue'
|
import GpuActivityPanel from '../components/settings/GpuActivityPanel.vue'
|
||||||
import DownloadsActivityPanel from '../components/settings/DownloadsActivityPanel.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 { useTabQuery } from '../composables/useTabQuery.js'
|
||||||
import { useMLStore } from '../stores/ml.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 system = useSystemStore()
|
||||||
const mlStore = useMLStore()
|
const mlStore = useMLStore()
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ describe('router', () => {
|
|||||||
expect(router.currentRoute.value.query.post_id).toBe('7')
|
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', () => {
|
it('series-read is an immersive route', () => {
|
||||||
const r = router.resolve('/series/5/read')
|
const r = router.resolve('/series/5/read')
|
||||||
expect(r.name).toBe('series-read')
|
expect(r.name).toBe('series-read')
|
||||||
|
|||||||
Reference in New Issue
Block a user