From ba2eb4d379868c73addf56482f363b9867c8e109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Sun, 18 Oct 2026 19:54:28 +0000 Subject: [PATCH] Make the console speak the backend's telemetry protocol The telemetry store and the printer card described a protocol no backend ever spoke. The store expected `{ type, data: { printerId, progressPct, state } }` with a closed five-value state enum; `/ws/telemetry` sends `{ type: "printer.telemetry", printer: {...} }` with `id`, `progressPercent` and an open `status`, and prefaces it with a `ready` frame. The card also displayed `printer.model`, which the printer resource has never had - it has `vendor`. Authentication was the other half: the socket now connects with the subprotocol list `["bearer", token]`, which is what the backend gates the upgrade on, and never a query parameter. The token is read from the session on each attempt, because supabase-js refreshes it hourly and a reconnect replaying the first token would work right up until it mattered. With no session it does not connect at all, and app.vue drives connect/disconnect from the session rather than a one-shot onMounted. Reconnection is now backed off from 1s to 30s instead of a flat 3s. A browser cannot see why an upgrade failed - a rejected token and an unreachable server are both just onerror then onclose - so a token the server will never accept previously retried every 3s for as long as the tab stayed open. `disconnect()` no longer reconnects the socket it just closed, via a `wanted` flag; handlers from a replaced socket are ignored. Null is no longer rendered as zero. A claimed printer whose gateway has never reported has no temperature, and 0 degrees is a different claim: null readings stay out of the graph's history, the labels show a dash, the progress bar is omitted rather than drawn empty, and a null status reads "unknown" rather than the old "offline", which asserted something nobody had observed. `lastSeenAt` is shown, since it is what separates an idle printer from one nothing has heard from in an hour. The card takes one `printer` prop instead of a row plus a `live` overlay - the push and the REST row are the same shape by design, so pages/index.vue replaces one with the other and @continuum/ui stays plain Vue that knows nothing of stores or the API. Status styling now covers all six values of the backend's printer_status enum; paused and maintenance previously drew as ordinary text. Types are hand-mirrored in packages/ui/src/types.ts from continuum-schemas, the convention this project already uses across TS and Rust. @continuum/ts-types is in another repository and is not resolvable here; a `link:` out of the repo would break on any checkout not laid out like this machine's. Field names are verified identical to the schema's. Typechecks clean (nuxt typecheck, vue-tsc). Not exercised at runtime; the protocol it now implements is the one verified live by continuum-backend's scripts/e2e-edge.sh. --- apps/web/app.vue | 18 +- apps/web/pages/index.vue | 44 +++-- apps/web/stores/telemetry.ts | 159 +++++++++++++----- packages/ui/src/components/PrinterCard.vue | 90 +++++++--- .../ui/src/components/TemperatureGraph.vue | 16 +- packages/ui/src/index.ts | 1 + packages/ui/src/types.ts | 50 ++++++ 7 files changed, 282 insertions(+), 96 deletions(-) create mode 100644 packages/ui/src/types.ts diff --git a/apps/web/app.vue b/apps/web/app.vue index 76577a7..d9673c2 100644 --- a/apps/web/app.vue +++ b/apps/web/app.vue @@ -3,9 +3,17 @@ import { useTelemetryStore } from "~/stores/telemetry"; const telemetry = useTelemetryStore(); const user = useSupabaseUser(); +const session = useSupabaseSession(); +// Driven by the session rather than a one-shot onMounted: the session can +// arrive after mount, and sign-out should drop the socket instead of leaving +// it running on a token that is about to stop being valid. onMounted(() => { - telemetry.connect(); + watch( + () => session.value?.access_token, + (token) => (token ? telemetry.connect() : telemetry.disconnect()), + { immediate: true }, + ); }); onBeforeUnmount(() => { @@ -18,7 +26,13 @@ onBeforeUnmount(() => {
Continuum - {{ telemetry.connected ? "Live" : "Reconnecting…" }} + {{ + telemetry.link === "connected" + ? "Live" + : telemetry.link === "connecting" + ? "Connecting…" + : "Offline" + }} {{ user.email }}
diff --git a/apps/web/pages/index.vue b/apps/web/pages/index.vue index 4e0a54e..71e3565 100644 --- a/apps/web/pages/index.vue +++ b/apps/web/pages/index.vue @@ -1,28 +1,31 @@ @@ -81,6 +102,14 @@ const state = computed(() => props.live?.state ?? "offline"); font-size: 0.8rem; } +/* + * One rule per value of the backend's `printer_status` enum - offline, idle, + * printing, paused, error, maintenance - plus the "unknown" this card + * substitutes for a null status. The enum was only two thirds covered, so a + * paused printer and one in maintenance drew in the default colour and read + * as ordinary. `status` is not a closed enum on the wire, so anything new + * still falls back to that default rather than breaking. + */ .printer-card.is-printing .printer-card__state { color: #35d07f; } @@ -89,7 +118,16 @@ const state = computed(() => props.live?.state ?? "offline"); color: #f2604c; } -.printer-card.is-offline .printer-card__state { +.printer-card.is-paused .printer-card__state { + color: #f0b429; +} + +.printer-card.is-maintenance .printer-card__state { + color: #9f7aea; +} + +.printer-card.is-offline .printer-card__state, +.printer-card.is-unknown .printer-card__state { color: #6b7280; } diff --git a/packages/ui/src/components/TemperatureGraph.vue b/packages/ui/src/components/TemperatureGraph.vue index d5f5cee..9aab525 100644 --- a/packages/ui/src/components/TemperatureGraph.vue +++ b/packages/ui/src/components/TemperatureGraph.vue @@ -1,9 +1,13 @@