From 243f765c0046d4dccc620db79fbb74014daea092 Mon Sep 17 00:00:00 2001 From: htjulia Date: Mon, 21 Sep 2026 15:38:43 +0900 Subject: [PATCH 1/3] feat(stream): read the connection ack instead of assuming it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The platform now opens every stream connection with a `ready` frame (protean 237bcd0, #80) stating what it is, what it is recording, how often it pushes and how much it retains. Four things the console had been asserting on its own are now read from it, and where the platform says nothing the console says it does not know rather than falling back to the guess. - the LIVE badge printed "200 buffer" on every platform — a ring size this console had never been told and could not see. It prints the stated `capacity`, or nothing at all. - the silence watchdog was 6s because the platform was assumed to push at 1Hz. It is six times the stated `tickMs`, clamped to 2-60s: a few milliseconds would trip on scheduler jitter and an hour would never trip, and this is the only signal that a platform is gone. - the module table read an empty `metrics` array as "switch this on", which is equally true of a platform that is on and idle — and it told operators to enable a setting that was already enabled. It reads `metricsEnabled`. Its "and restart" was wrong too: the key is live. - the empty trace table promised "requests appear here as the platform serves them", which nothing keeps while `trace.enabled` is false. It reads `tracesEnabled`. `tracesEnabled` is also read before `metricsEnabled` in the module panel. Per-module metrics are aggregated from recorded requests, and the contract states `metricsEnabled` as the effective value — so a platform with recording off reports metrics off as well, and that value read on its own would produce "turn it on and rows appear": an instruction that changes nothing beside a promise nothing can keep. The ack is display and diagnostics only — no behaviour branches on which implementation answered. Its values are connection-scoped and dropped on reconnect, because they are live settings on the platform and the last connection's answers may no longer be true. A platform that sends no ack is unchanged: Go still sends the older `hello`, Rust sends nothing, and both captures stay in the fixtures as that axis. The protean fixture is the real connect bytes, captured 2026-09-21 against examples/quickstart. Tags: #stream #honesty Co-Authored-By: htjulia --- CHANGELOG.md | 8 + README.md | 34 ++- src/App.tsx | 11 +- src/components/module-table.tsx | 100 +++++++- src/components/options-menu.test.tsx | 1 + src/components/platform-identity.test.tsx | 187 +++++++++++++++ src/components/top-bar.tsx | 48 +++- src/components/trace-table.tsx | 25 +- src/hooks/ready-ack.test.ts | 265 ++++++++++++++++++++++ src/hooks/trace-history.test.ts | 19 +- src/hooks/use-console-data.ts | 146 ++++++++++-- src/lib/types.ts | 27 +++ src/test/stream-fixtures.ts | 71 +++++- 13 files changed, 884 insertions(+), 58 deletions(-) create mode 100644 src/components/platform-identity.test.tsx create mode 100644 src/hooks/ready-ack.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c94ec6f..c8baee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Live SSE stream (`/platform/traces/stream`): a single connection multiplexes `trace` / `metrics` / `modules` / `summary` events pushed ~1×/s, with manual **Connect / Disconnect** stream control (`LIVE` / `PAUSED`). +- Connection ack (`ready`): the platform states its identity and version, its + trace-ring `capacity`, whether traces/metrics are recording, and its push + period (`tickMs`) as the first frame of every connection. The header shows the + platform it is talking to and the real ring size, the module table says whether + metrics are off or merely idle instead of inferring it from an empty array, and + the silence watchdog is sized from the stated period rather than an assumed + 1Hz. Platforms that send no ack are unchanged — the console reports what it was + not told as unknown. - Windowed header KPIs: the `summary` event drives the trend sub-lines (requests / error-rate / p95 vs the previous window) and the active-module split by isolation mode. Trend fields are nullable — a muted placeholder is shown diff --git a/README.md b/README.md index d798add..c7ac52e 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,13 @@ in logs. - **Live SSE stream** — one connection to `/platform/traces/stream` multiplexes `trace` / `metrics` / `modules` / `summary` events pushed ~1×/s, with manual **Connect / Disconnect** control. Replaces the previous 5s REST polling. +- **Connection ack** — the platform's `ready` frame names itself and its version, + states its trace-ring capacity, whether traces and per-module metrics are + recording, and its push period. The console reports those instead of assuming + them: the silence watchdog is sized from the stated period, the header prints + the stated ring size, and the module table stops guessing why it is empty. A + platform that sends no ack keeps working — the console then says it does not + know rather than falling back to a number nobody gave it. - **Live / sample auto-fallback** — when a platform is reachable, data is `live`; when none is running, the console falls back to grounded mock data so the UI is always explorable. The top bar shows a **LIVE** vs **SAMPLE DATA** badge. @@ -180,15 +187,40 @@ src/ ## Platform API surface The live path is a single SSE connection to `GET /platform/traces/stream`, which -multiplexes four named events: +opens with an acknowledgement and then multiplexes four named events: | Event | Maps to | Notes | |---|---|---| +| `ready` | `StreamReady` | Connection ack, first frame, repeated on every reconnect. Optional: platforms that predate it are supported. | | `trace` | `RequestTrace[]` | Incremental delta since the last seq (initial snapshot on connect). | | `metrics` | `ModuleMetricsSnapshot[]` | Full per-module snapshot each tick. Opt-in via `protean.trace.metrics.enabled`. | | `modules` | `ModuleStatus[]` | Joined into the module table for isolation mode / trust tier. | | `summary` | `TraceSummary` | Windowed KPI aggregate + trend vs the previous window + active-module split by mode. | +The `ready` ack is not a data frame. It carries what a client must learn once, at +connect time, and cannot derive from the stream afterwards — on a quiet platform +nothing else arrives at all, so it is also the only thing separating *the stream +is open* from *the platform is running*: + +``` +event: ready +data: {"platform":"protean","platformVersion":"0.1.0","tracesEnabled":true, + "metricsEnabled":false,"buffered":5,"tickMs":1000,"capacity":200} +``` + +| Field | What the console does with it | +|---|---| +| `platform` / `platformVersion` | Shown beside the origin in the header. **Display only** — no behaviour branches on which implementation answered. `null` version is rendered as unknown, never replaced with a guess. | +| `tracesEnabled` / `metricsEnabled` | Separates "recording is off" from "recording is on and idle" — the two an empty panel cannot tell apart. `tracesEnabled` is read first: per-module metrics are aggregated from recorded requests, so with recording off no rows can appear whatever the metrics flag says. | +| `buffered` | Read, but not acted on: it counts the rows of the `trace` frame that follows (not the ring's size), and those rows arrive regardless. | +| `tickMs` | Sizes the silence watchdog (6 periods, clamped to 2–60s) instead of assuming 1Hz. | +| `capacity` | The ring size as of this connection, printed in the `LIVE` badge. | + +Fields may be added later; the console ignores the ones it does not recognize. +Every value is connection-scoped — the console drops them when the stream is +rebuilt and re-learns them from the next ack, because they are live settings on +the platform. + The read-only REST surface (`GET /platform/traces`, `/platform/traces/metrics`, `/platform/modules`, `/platform/modules/{id}/routes`) is still mirrored in `api.ts` as a fallback and for the module-routes drawer. diff --git a/src/App.tsx b/src/App.tsx index 936d793..a11503f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,7 +32,7 @@ export default function App() { const [selectedModuleId, setSelectedModuleId] = useState(null) const { settings, save: saveSettings } = useSettings() - const { data, conn, channels, streaming, setStreaming } = useConsoleData(settings) + const { data, conn, channels, platform, streaming, setStreaming } = useConsoleData(settings) const disconnected = conn.status === 'disconnected' // Retain trace history in IndexedDB for a real platform; pass mock through. @@ -68,6 +68,7 @@ export default function App() { onToggleTheme={toggle} conn={conn} channels={channels} + platform={platform} storage={traceStore.storage} settings={settings} onSaveSettings={saveSettings} @@ -99,6 +100,8 @@ export default function App() { +