Repository navigation
fix(nodes): new boards counted everywhere, one fault order and a log of the physical bus, safe add all and un-hide all - #155
Merged
Merged
Conversation
… or stop early, dialogs show the current board (#153) Part of fix-track chunk 4 (#133). It covers the items X6 / R8, R6, N4, R9 / R10 and C5, as decided on 2026-10-16. The new-board state and event-log items (D1 / X3 / R2, S6, X8 / C6, D8 / S5 / C4) are in a separate PR. Nothing else changes. ## 1. X6 / R8: one fault order | | Today | After | |---|---|---| | **Order** | The Log listed a board's own flags as bus-off, error-passive, degraded, overheat. The Cockpit listed them as bus-off, fault, overheat, error-passive. | The Cockpit's order everywhere: bus-off, fault, overheat, error-passive. | | **Owner** | Three hand-written sequences (`views.ts`, `panels.ts`, `adopt.ts`) and the log's walk over `STATUS_FLAGS`. | `nodes/faults.ts NODE_FLAG_ORDER` and its helpers (`raisedNodeFlags`, `nodeFlagWords`, `nodeFlagChips`, `NODE_FLAGS`). The log (`node-events.ts`), the tile and Attention (`views.ts`), the status boxes (`panels.ts`) and the Adopt evidence (`adopt.ts`) all read them. | | **Vocabulary** | `events/status-flags.ts STATUS_FLAGS` was an ordered list. | It is now a record keyed by flag, with no order. `gateway-health.ts` looks labels up by key and keeps its own documented Gateway order. | The PDU flag text in `faults.ts` was written out twice: once for the Cockpit and once for the log, in the same order. The Cockpit now reuses the log's tables, so its output is unchanged. ## 2. R6: Add all can't double-run | | Today | After | |---|---|---| | **Dialog** | Add all had no busy flag, so a double click sent it twice. `done` was set from a stale closure. | One busy flag for the whole dialog, lifted into `AdoptDialog`. A ref refuses a second click before React re-renders. While Add all or Adopt runs, Add all, Later and Adopt / Add to cockpit are all disabled. `done` updates are functional. | | **Main** | `addAllInTurn` could run again while one was running. | `mapping-sequence.ts MappingSequences` (NodeHub and the component mock) ignores a second Add all, or a second Un-hide all, while the same one is running. It answers `{ ok: true, done: [] }`. The two sequences don't block each other. | ## 3. N4: Un-hide all goes on and reports | | Today | After | |---|---|---| | **Main** | It stopped at the first board it couldn't un-hide and never tried the rest. | Every board is tried. The refused case of `NodeBatchResult` gains `failed`: every refusal, in order. `nodeId` and `error` are still the first. Add all still stops at its first refusal, with one entry. | | **Settings › Hidden boards** | It showed the one error. | The existing error line (`set-err`) lists each failure: `Couldn’t un-hide #8 Right_Radar: <reason>`. A single Un-hide still shows main's error alone. No new UI element. | The API guard header (`ipc.compat.test-d.ts`) records the change to the type. ## 4. R9 / R10: the dialogs show the current board and count | | Today | After | |---|---|---| | **Reset… review** | It fetched the preview again on every `nodes:model` push (roster polls, PDU pages), with no stale guard. | It fetches again only when the inputs change. The key is `settings-view.ts resetPreviewKey`: the mapping as the snapshot shows it (each board's name, console, PDU and source, the hidden ones, and the differing count). An answer that a newer fetch superseded is dropped. | | **Adopt count** | The header counted every listed row ("6 boards…"), while the button that opened it said "New on the bus · 3". | The header reads `attention.newCount`, the same field as the button. | | **Adopt board** | A new request to open the dialog (a cog, or Connection's "›") while it was open kept it on the board it was showing. | Each request remounts the dialog (`key`) on the board asked for, as the latest push has it. | ## 5. C5: an older snapshot never wins - **Main.** NodeHub stamps a rising `rev` on every model it pushes (`NodeModel.rev`, optional). The `nodes:get` answer carries the rev of the last push. - **Renderer.** `nodes/store.ts` drops a `nodes:get` answer or a `nodes:model` push that is older than the model it holds. It also drops a `cockpit:snapshot` whose rev is older than the one it holds. - **The mock** stamps revs the same way NodeHub does. - **The other consumers.** `useNodesPulled` uses its own pull only as a "data arrived" signal and writes nothing, so no change there. The mapping intents' `model` answers aren't read by the renderer. Everything the screens show comes from the snapshot, which is now rev-guarded. ## Tests changed on purpose | Test | Was | Now | Why | |---|---|---|---| | `events.test` `raises and clears bus-off, error-passive (with TEC), overheat` | bus-off, error-passive, overheat | bus-off, overheat, error-passive | X6 / R8 | | `mapping-sequence.test` `unhideInTurn … stops at the first refusal` | stops at #30, #41 not tried | goes on, lists #30 and #52 in `failed` | N4 | | `mapping-sequence.test` Add all refusal | no `failed` | `failed: [{ 7 }]` | N4 (the shape of the result) | | `node-hub.test` Add all / Un-hide all | no `failed`; read-only un-hide stopped at #1 | `failed` added; #2 is tried after #1 | N4 | | T10 `lists every new board…` | "6 boards this computer hasn’t added yet" | "3 boards…", the same as "New on the bus · 3" | R10 | ## New tests - **`nodes/faults.test.ts`**: a board with several flags. The log rows, the status boxes, the Adopt evidence and the tile chip all come out in the one order. - **`nodes/mapping-sequence.test.ts`**: `MappingSequences` ignores re-entry, is free again after it finishes or after a refusal, and the two sequences are independent. - **`nodes/mapping-sequences.seam.test.ts`** (via `makeServices`, which gained the add-all and unhide intents): - two Add all invokes in flight: the second is ignored and each board is adopted once; - Un-hide all with a refused id in the middle: the others are un-hidden and the refusal is listed. - **`node-hub.test`**: every model pushed carries a rising rev, and `nodes:get` answers the last push's rev. - **T11**: a double click on Add all runs it once, and the dialog is busy until it lands. - **T12**: Un-hide all with one board failing un-hides the rest and lists the failure. - **T30**: the review asks main again only when the mapping changes. - **T10**: a request to open another board while the dialog is open lands on that board. - **`ipc-adapter.test`**: a pull answered behind a newer push is dropped, and so is an older Cockpit snapshot. ## How verified - `npm run lint`, `npm run lint:deps`, `npm run typecheck` and `npm test` all pass (1409 tests). - Each new renderer and store test was run against the code from before its fix, and failed there: - T11 failed with the old `AdoptDialog`; - T30 with the old `ResetDialog` / `CockpitSection`; - both R10 tests in T10 with the old `Cockpit.tsx`; - both C5 cases with the old `store.ts`. - The fault-order test fails against the old `STATUS_FLAGS` walk. - Not run on hardware. The #133 hardware checklist items "fault order matches", "double-click Add all runs once" and "Un-hide all with one failing board" are still to do.
…, one log of the physical bus (#154) Fix-track chunk 4 (#133), the new-boards and event-log items. Behaviour changes on purpose (decided 2026-10-16); nothing else does. Rebased on `fix/chunk-4` after #153 (one fault order); its changes are kept as they are. ## 1. D1 / X3 / R2: strays on every tab | | Today | After | |---|---|---| | Strays | The 5 s "UNIDENTIFIED" (or SECOND GATEWAY / UNKNOWN BOARD TYPE) warning counted on the Cockpit only. The Overview tile read "no nodes known", with no banner; Connected nodes had no row. | One `newState(entry, now, live)` in `node-model.ts`: `news` (adoptable), `identifying` (with `strayAt`), `stray`, or none. `isReportableNew` is built on it, and `attention.ts strayNew` reads it. `buildCockpitSnapshot` hands the same `strays` to `overviewSummary` and `nodesSummary`. The Cockpit, the Overview (tile and banner) and Connection (counts plus a stray row with chip `… · can’t be adopted`, no zone) all count it after 5 s. `NodesSummary.known` is the band's total, so a lone stray isn't hidden behind "No nodes reported". | `IDENTIFY_WAIT_MS` and `strayKind` moved into `node-model.ts`. `strayFlag` is `strayKind` in capitals. ## 2. S6: grace from coming online | | Today | After | |---|---|---| | Grace | Counted from `firstSeenAt`, which an absent roster row also sets. A board listed absent and coming online later was flagged at once. | `NodeRoster` keeps `NodeSeen.onlineSince`: set on a present row after an absent one (or the first present row), cleared when the board goes absent (delta or END). The model copies it onto new entries (`NodeModelEntry.onlineSince`), and `newState` counts the grace from it. | ## 3. X8 / C6: "from NODE_IDS.md" wording | | Today | After | |---|---|---| | A registry board waiting to be added | `nodeHealth` flag `NOT IN REGISTRY`, the `nodeFaultBoxes` box "Not in registry", log detail `in NODE_IDS.md` / `not in registry`. The Overview already said "from NODE_IDS.md". | One `newReason(entry)` in `node-model.ts`: `from NODE_IDS.md` / `not in NODE_IDS.md`. The flag is `FROM NODE_IDS.md` / `NOT IN NODE_IDS.md`. The box title is `From NODE_IDS.md` (a registry board's text no longer says it isn't listed). The log's "new on the bus" detail reads the same. | A probe test (`main/nodes/phase0-probes` "X8 / C6") scans every `.ts`/`.tsx` in `src` for `not in registry` (any case) and the old log literal `'in NODE_IDS.md'`, and fails on a hit. It was shown to fail when `// NOT IN REGISTRY` was appended to `views.ts`. **Left alone:** `AdoptDialog.tsx`'s queue label `<name> · in NODE_IDS.md` is the renderer's own wording for a registry board in the Adopt queue. It was out of scope (that dialog was another PR's file), so it isn't in the probe. ## 4. D8 / S5 / C4: the log is the physical bus | | Today | After | |---|---|---| | Boards outside your cockpit | Their offline / bus-off / flag rows were errors and warnings like your own boards' rows. A stray never got any row. | Every board on the bus is still logged. A row about a board outside your cockpit is at **info**, with the detail suffix `not in your cockpit` (one `row()` in the tracker). A stray gets one `new on the bus · <unidentified \| second Gateway \| unknown board type> · can’t be adopted` row at info once `newState` says `stray`. If it later identifies as adoptable, it gets its `new` row then. | | "N online" | Counted every board, with nothing to tell them apart. | Still counts the whole bus, now with `· K not in your cockpit` when some aren't yours. | | "New" | Re-derived in NodeHub (`isNew`: not known and an adoptable `kindOf`). Only checked on status rows, so a board identified only by telemetry got no row until its next roster row. | `announce()` reads `newState` from the model entry, built at most once per line, so it isn't re-derived. It is checked on status **and** telemetry lines: a PDU identified by its pages gets its row at those pages. | | "Seen new" record | Survived Forget, so Forget then re-plug logged nothing new. | `NodeEventTracker.cockpitChanged()` runs after every successful mapping change. It drops the record for boards now in your cockpit, so a forgotten board that returns is new again. | | Telemetry-only boards | No last seen, and no online / offline rows. | NodeHub's `busTransitions` (was `rosterTransitions`) uses `onBusIds`: the one `availabilityOf` rule over roster ∪ telemetry. It runs on roster changes and on page takes, so last seen and un-hide follow pages too. `pagesOnline()` gives a board online by its pages alone (`onlineByPages`) `online` / `offline · no telemetry` rows. If the roster later lists it, no second `online`. | | Degraded | A new fault id while still degraded logged another `degraded` row. | Once per episode: raised, then cleared. | ## Tests changed on purpose | Test | Was | Now | Why | |---|---|---|---| | T1 `the Overview does not count the same stray (X1 today)` | "no nodes known", no banner | Banner `1 node needs attention — 1 warning (NODE_ID 9)` | D1 | | T1 `the 5 s grace counts from firstSeenAt … (F26 today)` | Stray at once | No stray at first, a stray 5 s after `onlineSince` | S6 / F26 | | T3 `the Overview doesn’t count a stray (X1 today)` | `—` / "no nodes known" | Tile `1 / 1`, banner names NODE_ID 9 | D1 | | T4 `a board still identifying, or one that can’t be adopted, is not mentioned` | No status role at all | Notice still absent; the banner names the stray (fake clock at T0 so #9 is still identifying) | D1: the stray is a warning, not news | | T34 the agreement cases (header + 2 tests) | Overview / Connection without the stray | All three count it (`4 / 7`, Connection 2/2/2 with the stray's row; next push `6 / 7`, 5/1/0) | D1 | | `on-the-bus.test` link up | Connected nodes `known 2`, rows `[2]` | `known 3, warn 1`, rows `[2, 9]` | D1 (Connection strays-absent case) | | `overview-summary.test` `… without strays` | Band and items without strays | Same equality, `strays` passed; plus a new stray case | D1 | | events `lookup()` stub (and the #153 `faults.test` stub, call-site only) | `isNew` / `unmapped` / `inRegistry` | `outside` / `entry` (the model entry) | "New" from `newState` | | events: registry / not-in-registry details | `in NODE_IDS.md` / `not in registry` | `from NODE_IDS.md` / `not in NODE_IDS.md` | X8 | | events `reports a new node once per app run…` | `roster: 1 node online` | `… · 1 not in your cockpit` | D8 label | | events `a new node coming back in a later port session…` | `online [info]`, `offline · no heartbeat [error]` | `… · not in your cockpit [info]` | D8 | | events `degraded … a new fault id is a new row` | Second row for fault 9 | No row; after a clear, a new episode logs | Degraded once per episode | | `node-hub.seam` B33 | No row at the pages; one at the next status row | One row at the pages, none after | C4 | | main `phase0-probes` C4 | Same as B33 | Fixed; plus Forget → re-plug logs "new" again; plus the X8 source probe | C4 / X8 | | renderer `phase0-probes` D1, S6, C6 | Today's behaviour | The fixed behaviour | D1 / S6 / X8 | | `node-hub.test` S1 kind stored | `{ added, kind }` after PDU pages | Also `lastSeen` | Telemetry-only boards update last seen | | `node-status-wire.test` seen | `{ firstSeenAt, reportedAt }` | Includes `onlineSince` (reset on coming back) | S6 | | T22 log fixture | `detail: 'in NODE_IDS.md'` | `from NODE_IDS.md` | Fixture mirrors main's wording; the X8 probe covers it | **Inputs only, assertions unchanged:** `scenarios.newBoard` also sets `onlineSince` (its doc already said "online since firstSeenAt"), and the `adopt.test` / `cockpit-host.test` / `on-the-bus.test` stray fixtures set `onlineSince`. `nodesSummary` / `overviewSummary` gained the `strays` argument (`[]` in the old cases). The **NODE_ID 0 overview cases** (`takes / treats NODE_ID 0 like any other node`) are unchanged and still pass. Nothing here changes how the summaries treat a NODE_ID 0 entry, and the model has excluded node 0 since #344. ## New tests - `node-model.test`: `newState` (news / stray / identifying with `strayAt`, none off the bus or unplugged), `onlineSince` on the model, `newReason`, `strayKind`, `onlineByPages`. - `node-status-wire.test`: `onlineSince` cleared by absent row or END, reset on coming back. - `adopt.test`: grace from `onlineSince`. - `overview-summary.test` and `connection-summary.test`: a stray counted with its row. - events: outside rows at info and labelled; baseline count; stray announcements; stray → news; telemetry announce; `cockpitChanged`; `pagesOnline`. - New `main/nodes/new-boards.seam.test.ts`, through `makeServices()` over the fake serial port: - a stray on all three tabs after 5 s; - the grace from coming online; - the `FROM NODE_IDS.md` flag and log row; - outside rows at info and the roster label; - Forget → re-plug logs new again; - a pages-only PDU's last seen and its online / offline rows. ## How verified - `npm run lint`, `npm run lint:deps`, `npm run typecheck`, `npm test`: all green (1434 tests). - Not run on hardware. The chunk's hardware checklist is in #133. API guard: header note only. `NodeModelEntry` and `NodeSeen` gained optional `onlineSince`; there is no channel or intent change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Chunk 4 of the fix track (#135): new boards, event log and cockpit actions. Closes #133.
Item PRs squash-merged into
fix/chunk-4:newState: the unidentified warning counts on Cockpit, Overview and Connection after 5 s; the 5 s grace counts from when a board comes online (reset on each offline → online); "from NODE_IDS.md" wording everywhere (onenewReason, probe test); the event log is the physical bus — boards outside your cockpit logged at info with "· not in your cockpit", a telemetry-only board gets its "new" row and online/offline rows, "new" is worked out once and cleared on Forget, degraded is logged once per episode.Tested on the rig before merge (checklist in #133).
BEGIN_COMMIT_OVERRIDE
fix(nodes): one fault order, add all and un-hide all can't double-run or stop early, dialogs show the current board
fix(nodes): new boards counted on every tab, grace from coming online, one log of the physical bus
END_COMMIT_OVERRIDE