feat: the dot beside the brand now means the whole stack (milestone 365 step 4)
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 5s
CI / extension-version (push) Successful in 5s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 9s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 31s
Build images / build-web (push) Successful in 55s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 1m43s
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 5s
CI / extension-version (push) Successful in 5s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 9s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 31s
Build images / build-web (push) Successful in 55s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 1m43s
The ask was a surface AND a path. The path is the part that was missing — everything that could answer "is it running" lived inside Settings, which you only open once you already suspect something. **Re-used the indicator that already existed rather than adding a fourth.** There were three partial surfaces: TopNav's health dot, PipelineStatusChip's pulse, and the Settings Activity tab. None answered "is every part alive", and a fourth would have made the question harder to answer, not easier. TopNav's dot read /api/health — a no-DB liveness check proving only that the WEB container is serving. Green there while a worker was dead is exactly what it looked like, and a green dot beside the product name gets read as "everything is fine". It now reflects the whole-stack verdict, and it is a link: the place someone already looks when they suspect something is now also the way to the detail. The tooltip names the actual problem. "Scheduler has not checked in for 6 min" sends someone somewhere; "something is unhealthy" sends them hunting. /system is deliberately NOT in the nav row — TopNav builds that from routes with a meta.title, and a sixth top-level tab for a page visited twice a year costs more attention than it returns. It is reached from the dot. The page lists every learned part with its state as a sentence rather than a chip, and prints the staleness thresholds it was judged by, taken from the endpoint so the UI keeps no second copy of them. PipelineStatusChip still hand-rolls its own 3-minute scheduler window; that is now a duplicate of a threshold the server owns, and worth collapsing once this has been watched working. The stores stay separate on purpose: system.js is "can I reach the API", systemActivity.js is "what is the pipeline doing", systemHealth.js is "is anything broken". Running and alive fail independently — an idle stack with a dead worker looks identical to a healthy one on every activity surface, which is the whole reason this milestone exists. Not yet verified against a real stopped service. Rule 12 keeps a local stack out of it, and frontend CI has no Vue type-check or visual regression, so this needs an operator look rather than a green lane. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTjbZZ6JirCMSaJzQV1RhA
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<v-container class="py-6" style="max-width: 900px">
|
||||
<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…' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="fc-sys__lede text-body-2 mb-5">
|
||||
Every moving part of FabledCurator and whether it is still checking in.
|
||||
Parts are learned as they appear, so anything that has run at least once
|
||||
stays listed — that is what lets a stopped one be noticed rather than
|
||||
simply vanishing.
|
||||
</p>
|
||||
|
||||
<v-alert
|
||||
v-if="store.lastError" type="error" variant="tonal" density="compact" class="mb-4"
|
||||
>
|
||||
Could not reach FabledCurator: {{ store.lastError }}
|
||||
</v-alert>
|
||||
|
||||
<v-card v-else variant="flat" class="fc-sys__card">
|
||||
<div v-if="!store.parts.length" class="pa-6 text-center fc-sys__muted">
|
||||
Still gathering — this fills in on the first check.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="part in store.parts" :key="part.key"
|
||||
class="fc-sys__row" :class="`fc-sys__row--${part.state}`"
|
||||
>
|
||||
<span class="fc-sys__dot" :class="`fc-sys__dot--${part.state}`" />
|
||||
|
||||
<div class="fc-sys__body">
|
||||
<div class="fc-sys__name">
|
||||
{{ part.name }}
|
||||
<span class="fc-sys__kind">{{ kindLabel(part.kind) }}</span>
|
||||
</div>
|
||||
<!-- The sentence, not just a chip. At the moment someone is deciding
|
||||
whether to go and open Portainer, "has not checked in for 6 min"
|
||||
is the thing that answers them. -->
|
||||
<div class="fc-sys__detail">{{ part.detail }}</div>
|
||||
</div>
|
||||
|
||||
<div class="fc-sys__meta">
|
||||
<div v-if="part.last_seen_at" :title="part.last_seen_at">
|
||||
seen {{ formatRelative(part.last_seen_at) }}
|
||||
</div>
|
||||
<div v-if="part.latency_ms != null">{{ part.latency_ms }} ms</div>
|
||||
<div v-if="part.queues?.length" class="fc-sys__queues">{{ part.queues.join(', ') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<p v-if="store.thresholds" class="fc-sys__foot text-caption mt-4">
|
||||
A part is called stale after
|
||||
{{ Math.round(store.thresholds.stale_after_seconds / 60) }} min without a
|
||||
check-in and treated as stopped after
|
||||
{{ Math.round(store.thresholds.down_after_seconds / 60) }} min. The window
|
||||
is deliberately wide: a rolling deploy briefly runs two of a service and
|
||||
then neither, and an indicator that reddened on every update would stop
|
||||
being read.
|
||||
</p>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
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'
|
||||
if (kind === 'datastore') return 'datastore'
|
||||
return kind
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
store.refresh()
|
||||
timer = setInterval(() => { if (!document.hidden) store.refresh() }, POLL_MS)
|
||||
})
|
||||
onUnmounted(() => { if (timer) clearInterval(timer) })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-sys__lede, .fc-sys__muted, .fc-sys__checked, .fc-sys__foot {
|
||||
color: rgb(var(--v-theme-on-surface) / 0.66);
|
||||
}
|
||||
.fc-sys__checked { font-size: 0.78rem; }
|
||||
.fc-sys__card { background: rgb(var(--v-theme-on-surface) / 0.04); }
|
||||
|
||||
.fc-sys__row {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgb(var(--v-theme-on-surface) / 0.08);
|
||||
}
|
||||
.fc-sys__row:last-child { border-bottom: 0; }
|
||||
|
||||
.fc-sys__dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
|
||||
.fc-sys__dot--ok { background: rgb(var(--v-theme-success)); }
|
||||
.fc-sys__dot--stale { background: rgb(var(--v-theme-warning)); }
|
||||
.fc-sys__dot--down { background: rgb(var(--v-theme-error)); }
|
||||
.fc-sys__dot--unknown { background: rgb(var(--v-theme-on-surface) / 0.35); }
|
||||
|
||||
.fc-sys__body { min-width: 0; flex: 1 1 auto; }
|
||||
.fc-sys__name { font-weight: 600; }
|
||||
.fc-sys__kind {
|
||||
margin-left: 8px; font-weight: 400; font-size: 0.72rem; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; color: rgb(var(--v-theme-on-surface) / 0.5);
|
||||
}
|
||||
.fc-sys__detail { font-size: 0.82rem; color: rgb(var(--v-theme-on-surface) / 0.72); }
|
||||
|
||||
.fc-sys__meta {
|
||||
text-align: right; font-size: 0.75rem; flex: 0 0 auto;
|
||||
font-variant-numeric: tabular-nums; color: rgb(var(--v-theme-on-surface) / 0.6);
|
||||
}
|
||||
.fc-sys__queues { opacity: 0.75; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user