Skip to content

fix(nodes): one fault order, add all and un-hide all can't double-run or stop early, dialogs show the current board - #153

Merged
mottihoresh merged 2 commits into
fix/chunk-4from
fix/c4b-faults-dialogs
Oct 17, 2026
Merged

mottihoresh merged 2 commits into
fix/chunk-4from
fix/c4b-faults-dialogs

Conversation

@mottihoresh

Copy link
Copy Markdown
Contributor

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 fix: chunk 4 — new boards, event log and cockpit actions #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.

…he log

X6 / R8 (#133). The log listed a board's own flags as bus-off, error-passive,
degraded, overheat; the Cockpit as bus-off, fault, overheat, error-passive.

- nodes/faults.ts owns the one order (NODE_FLAG_ORDER) and the helpers built on it:
  raisedNodeFlags, nodeFlagWords (Adopt evidence), nodeFlagChips (tile and Attention).
- The event log walks NODE_FLAGS; the tile, the status boxes and the Adopt evidence
  read the same order. STATUS_FLAGS (events) is now a vocabulary by flag, with no order.
- The PDU's Cockpit flag text reuses the log's tables instead of a second copy.
…ialogs show the current board

#133 items R6, N4, R9 / R10 and C5.

- R6: one busy flag for the whole Adopt dialog; done updates are functional. Main's
  MappingSequences ignores a second Add all / Un-hide all while the same one runs.
- N4: Un-hide all tries every board; NodeBatchResult's refused case gained `failed`, and
  Hidden boards lists each board it couldn't un-hide.
- R9: the reset review re-fetches only when the mapping it is built from changes, and
  drops an answer a newer fetch superseded.
- R10: the Adopt dialog's count is the "New on the bus" button's; each request to open it
  starts it over on the board asked for.
- C5: every model main hands out carries a rising rev; the renderer drops an older model
  or Cockpit snapshot than the one it holds.
@mottihoresh
mottihoresh merged commit 74f5a93 into fix/chunk-4 Oct 17, 2026
3 checks passed
@mottihoresh
mottihoresh deleted the fix/c4b-faults-dialogs branch October 17, 2026 18:11
mottihoresh added a commit that referenced this pull request Oct 17, 2026
…, 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 added a commit that referenced this pull request Oct 18, 2026
…of the physical bus, safe add all and un-hide all (#155)

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

Item PRs squash-merged into `fix/chunk-4`:
- #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.
- #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
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.

1 participant