fix(web): keep health and status live everywhere they are shown
The SSE stream already carried health.changed, health.stale, signal.raised, signal.resolved and coverage.unmonitored, but two of the places that render health never listened for them. - The Fleet table refreshed only on entity.*, so its Health column sat at whatever it was when the page mounted while the map view beside it — which did listen — updated live. Health arrives on its own events, not entity.*. Coalesced on a 400ms timer because health.stale fires once per entity during a sweep, and refetching the whole fleet per event would mean a burst of identical requests. - The entity detail window loaded health, signals and monitoring once on open and never again, so a window left on screen kept showing the health it had at mount. That is the same staleness this whole change set has been about, reproduced one window at a time. Now scoped by entity_id, and re-reads only what a health or signal event can actually change rather than re-running the full 11-request load(). Verified against live prod: flipping lxc:apps healthy -> degraded -> healthy updated the Fleet table and an open detail window together, without a reload. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -200,22 +200,55 @@
|
|||||||
// which only became visible once running executions were shown at all.
|
// which only became visible once running executions were shown at all.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const ev = $liveEvents[0]
|
const ev = $liveEvents[0]
|
||||||
if (!ev || !ev.type.startsWith('execution.')) return
|
if (!ev) return
|
||||||
|
|
||||||
if (ev.type === 'execution.output') {
|
if (ev.type.startsWith('execution.')) {
|
||||||
const target = expandedExecution
|
if (ev.type === 'execution.output') {
|
||||||
if (target && ev.entity_id === target) loadExecutionLogs(target)
|
const target = expandedExecution
|
||||||
|
if (target && ev.entity_id === target) loadExecutionLogs(target)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!entity) return
|
||||||
|
// Only refetch for an execution this panel is actually showing, so an
|
||||||
|
// unrelated command elsewhere in the fleet doesn't cause a request here.
|
||||||
|
if (executions.some((e) => e.id === ev.entity_id)) refreshExecutions()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entity) return
|
// Health and signals for THIS entity. The panel loaded these once on
|
||||||
// Only refetch for an execution this panel is actually showing, so an
|
// open, so a window left on screen kept showing the health it had at
|
||||||
// unrelated command elsewhere in the fleet doesn't cause a request here.
|
// mount — the exact staleness the fleet-wide work was about, reproduced
|
||||||
if (executions.some((e) => e.id === ev.entity_id)) {
|
// one window at a time. Scoped by entity_id so an unrelated entity going
|
||||||
refreshExecutions()
|
// down elsewhere costs nothing here.
|
||||||
|
if (!entity || ev.entity_id !== entity.id) return
|
||||||
|
if (
|
||||||
|
ev.type === 'health.changed' ||
|
||||||
|
ev.type === 'health.stale' ||
|
||||||
|
ev.type.startsWith('signal.') ||
|
||||||
|
ev.type.startsWith('coverage.')
|
||||||
|
) {
|
||||||
|
refreshStatus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Re-reads only what a health or signal event can change, rather than
|
||||||
|
// re-running the full 11-request load().
|
||||||
|
async function refreshStatus() {
|
||||||
|
if (!entity) return
|
||||||
|
const id = entity.id
|
||||||
|
const slugAtStart = entity.slug
|
||||||
|
const [fresh, sig, ch] = await Promise.all([
|
||||||
|
fetchEntity(slugAtStart),
|
||||||
|
fetchEntitySignals(id),
|
||||||
|
fetchChecksForTarget(slugAtStart)
|
||||||
|
])
|
||||||
|
// The panel may have switched entity while these were in flight.
|
||||||
|
if (!entity || entity.id !== id) return
|
||||||
|
if (fresh) entity = fresh
|
||||||
|
signals = sig
|
||||||
|
checks = ch
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshExecutions() {
|
async function refreshExecutions() {
|
||||||
if (!entity) return
|
if (!entity) return
|
||||||
const id = entity.id
|
const id = entity.id
|
||||||
|
|||||||
@@ -177,10 +177,28 @@
|
|||||||
return unsubscribe
|
return unsubscribe
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Health arrives on its own events, not entity.*, so the table's Health
|
||||||
|
// column used to sit at whatever it was when the page mounted while the
|
||||||
|
// graph view beside it updated live. Coalesced because health.stale fires
|
||||||
|
// once per entity during a sweep, and refetching the whole fleet for each
|
||||||
|
// would mean a burst of identical requests.
|
||||||
|
let fleetRefresh: ReturnType<typeof setTimeout> | null = null
|
||||||
|
function refreshFleetSoon() {
|
||||||
|
if (fleetRefresh) return
|
||||||
|
fleetRefresh = setTimeout(() => {
|
||||||
|
fleetRefresh = null
|
||||||
|
loadEntities()
|
||||||
|
}, 400)
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const ev = $liveEvents[0]
|
const ev = $liveEvents[0]
|
||||||
if (!ev || !ev.type.startsWith('entity.')) return
|
if (!ev) return
|
||||||
loadEntities()
|
if (ev.type.startsWith('entity.')) {
|
||||||
|
loadEntities()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ev.type === 'health.changed' || ev.type === 'health.stale') refreshFleetSoon()
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredEntities = $derived.by(() => {
|
const filteredEntities = $derived.by(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user