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:
@@ -5,9 +5,12 @@
|
||||
<img src="/favicon.svg" alt="" class="fc-brand__glyph" width="22" height="22" />
|
||||
<span class="fc-brand__text">FabledCurator</span>
|
||||
</RouterLink>
|
||||
<span class="fc-health" :title="health.label">
|
||||
<RouterLink
|
||||
:to="{ name: '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>
|
||||
</span>
|
||||
</RouterLink>
|
||||
<PipelineStatusChip />
|
||||
</div>
|
||||
|
||||
@@ -64,13 +67,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import router, { FRONT_DOOR } from '../router.js'
|
||||
import { useSystemStore } from '../stores/system.js'
|
||||
import { useSystemHealthStore } from '../stores/systemHealth.js'
|
||||
import PipelineStatusChip from './PipelineStatusChip.vue'
|
||||
|
||||
const system = useSystemStore()
|
||||
const healthStore = useSystemHealthStore()
|
||||
|
||||
// Publish the nav's REAL height as --fc-nav-h so full-height workspaces
|
||||
// (Explore/Subscriptions) and sticky sub-headers pin to it exactly instead of a
|
||||
@@ -116,15 +121,55 @@ const settingsRoute = computed(() =>
|
||||
navRoutes.value.find(r => r.name === 'settings') || null
|
||||
)
|
||||
|
||||
// The dot beside the brand, and the only ambient signal that something in the
|
||||
// stack has stopped (milestone 365).
|
||||
//
|
||||
// It used to read /api/health — a no-DB liveness check that proves the WEB
|
||||
// container is serving and nothing else. Green there while the worker was dead
|
||||
// is exactly what it looked like, and a green dot next to the product name is
|
||||
// read as "everything is fine". It now reflects the whole-stack verdict.
|
||||
//
|
||||
// Deliberately re-using this element rather than adding a second indicator:
|
||||
// there were already three partial surfaces (this, the pipeline chip, the
|
||||
// Settings Activity tab) and a fourth would have made the question harder to
|
||||
// answer, not easier. This is the one that already occupied the slot.
|
||||
const health = computed(() => {
|
||||
if (system.healthy === null) {
|
||||
const overall = healthStore.overall
|
||||
if (overall === null) {
|
||||
return { icon: 'mdi-circle-outline', color: 'on-surface', label: 'checking…' }
|
||||
}
|
||||
if (system.healthy === true) {
|
||||
return { icon: 'mdi-circle', color: 'success', label: 'healthy' }
|
||||
if (overall === 'ok') {
|
||||
return { icon: 'mdi-circle', color: 'success', label: 'All parts running' }
|
||||
}
|
||||
return { icon: 'mdi-alert-circle', color: 'error', label: 'unreachable' }
|
||||
// Name what is wrong in the tooltip. "Something is unhealthy" sends someone
|
||||
// hunting; "Scheduler has not checked in for 6 min" does not.
|
||||
const worst = healthStore.problems[0]
|
||||
const others = healthStore.problems.length - 1
|
||||
const suffix = others > 0 ? ` (+${others} more)` : ''
|
||||
if (overall === 'down') {
|
||||
return {
|
||||
icon: 'mdi-alert-circle', color: 'error',
|
||||
label: (worst?.detail || 'A part has stopped') + suffix,
|
||||
}
|
||||
}
|
||||
if (overall === 'stale') {
|
||||
return {
|
||||
icon: 'mdi-alert', color: 'warning',
|
||||
label: (worst?.detail || 'A part is quiet') + suffix,
|
||||
}
|
||||
}
|
||||
return { icon: 'mdi-help-circle-outline', color: 'on-surface', label: 'Health unknown' }
|
||||
})
|
||||
|
||||
const HEALTH_POLL_MS = 15_000
|
||||
let healthTimer = null
|
||||
onMounted(() => {
|
||||
healthStore.refresh()
|
||||
healthTimer = setInterval(() => {
|
||||
if (!document.hidden) healthStore.refresh()
|
||||
}, HEALTH_POLL_MS)
|
||||
})
|
||||
onUnmounted(() => { if (healthTimer) clearInterval(healthTimer) })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -237,7 +282,14 @@ const health = computed(() => {
|
||||
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. */
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.fc-health:hover { background: rgb(var(--v-theme-on-surface) / 0.12); }
|
||||
.fc-nav-right {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user