Skip to content

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
mottihoresh merged 2 commits into
mainfrom
fix/chunk-4
Oct 18, 2026
Merged

mottihoresh merged 2 commits into
mainfrom
fix/chunk-4

Conversation

@mottihoresh

Copy link
Copy Markdown
Contributor

Chunk 4 of the fix track (#135): new boards, event log and cockpit actions. Closes #133.

Item PRs squash-merged into fix/chunk-4:

  • fix(nodes): one fault order, add all and un-hide all can't double-run or stop early, dialogs show the current board #153 — one fault order (bus-off → fault → overheat → error-passive) in the Log and the Cockpit; Add all can't double-run (dialog busy flag + main re-entry guard); Un-hide all goes on past a failure and lists every board it couldn't un-hide; Reset preview refetches only when the mapping changes; Adopt opens on the board asked for and its count matches the button; an older node model or cockpit snapshot never replaces a newer one.
  • fix(nodes): new boards counted on every tab, grace from coming online, one log of the physical bus #154 — one 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 (one newReason, 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

… 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.
@mottihoresh
mottihoresh marked this pull request as ready for review October 18, 2026 01:52
@mottihoresh
mottihoresh merged commit 96b4f42 into main Oct 18, 2026
4 checks passed
@mottihoresh
mottihoresh deleted the fix/chunk-4 branch October 18, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: chunk 4 — new boards, event log and cockpit actions

1 participant