Skip to content

Alert notebook: read-only render on #/triage (#4222) - #4368

Merged
erikdarlingdata merged 6 commits into
devfrom
feature/4222-alert-notebook-render
Sep 26, 2026
Merged

erikdarlingdata merged 6 commits into
devfrom
feature/4222-alert-notebook-render

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Refs #4222.

Why

Slice C of the alert-notebook MVP: the client render side. #/triage needs to render a read-only notebook document bound to one firing, sourced from GET /api/alert-notebook (PR #4366), while the saved-view notebook renderer keeps working unchanged.

What changes

  • wwwroot/js/pages/views.js: renderNotebookDoc is refactored to accept either a saved view (mode: "saved", behaviour unchanged) or an in-memory definition bound to a firing (mode: "alert", read-only): no fleet scope picker (the server is fixed, so no /api/fleet read), a header/status cell pair rendered directly from the endpoint's own facts (no read for those two), a read-type cell rendered through the shared renderPanel, a max-3 in-flight InFlightLimiter gating those read-cell loads, and "Save as notebook" / "Open live" controls.
  • wwwroot/js/pages/triage.js: tries GET /api/alert-notebook first; on success renders its definition through renderNotebookDoc in alert mode, showing alert, status, and notes[]. On a 404 (endpoint not on this build yet) it falls back to the existing /api/triage assembly, unchanged.
  • wwwroot/js/panels.js (renderPanel) and wwwroot/js/compose.js (renderComposedPanelCard): both now take an optional onSettled callback, fired once when a cell's load reaches a terminal state, so a caller with its own concurrency budget (the limiter above) knows when a slot frees. Every existing caller omits it — a no-op, no behaviour change for dashboards or saved-view notebooks.
  • Darling.Tests/AlertNotebookRenderClientTests.cs: new source-text pins over the shipped JS (no JS runner in this repo, matching the existing pattern in ViewTemplatesTests/ServerPageTabsTests).

Built against PR #4366 (feature/4222-alert-notebook-endpoint, head ee6387936) for the /api/alert-notebook response shape: { alert, status, notes[], template: {id, version}, definition: {kind:"notebook", cells:[...]} }, with cells[0] a header cell, cells[1] a status cell, and cells[2..] read cells. That shape has no composed cells and no per-cell range yet, so "Open live" is implemented as dropping each read cell's as_of param.

Out of scope, by design: status refresh on the 60s poll (needs slice d's poll guard on app.js's refresh(), not landed); templates for other alert families (slice B in the umbrella design); the read cell type's server-side validation and renderCell routing (slice a2, not landed — this PR calls the shared renderPanel directly in alert mode rather than through the dashboard's generic renderCell, since that routing doesn't exist yet on dev).

Lite parity: N/A (Darling web only).

Test plan

  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj -p:EnableWindowsTargeting=true — 0 Warning(s), 0 Error(s)
  • node --check on all four touched JS files, plus a Node ESM import resolution smoke check on views.js and triage.js (no missing-export/import errors)
  • AlertNotebookRenderClientTests (new) — builds; CI decides
  • Full Windows test suite — CI decides (this repo builds but cannot run tests on macOS)

CHANGELOG entry

SECTION: Added
ENTRY:

For the coordinator

  • This branch is pushed (feature/4222-alert-notebook-render, head 9f5d59fce) but built against Add the alert-notebook binding endpoint (#4222) #4366 while it was still unmerged. Re-verify the /api/alert-notebook response shape against Add the alert-notebook binding endpoint (#4222) #4366's actual landed contract before merge; the coordinator's own message gave the shape used here.
  • renderNotebookDoc's new opts object shape ({mode, definition, alert, status, notes, canEdit, catalog, provenance, onOpenLive} for alert mode) is exported from views.js — slice a2 (the generic read-cell renderCell routing) can reuse it once ValidateNotebookDefinition accepts read cells on dev; today this PR bypasses renderCell and calls renderPanel directly for alert-mode read cells since that routing isn't landed yet.
  • panelOrError/renderCell's saved-notebook path is completely untouched — no behaviour change there.
  • panels.js renderPanel(desc, onSettled) and compose.js renderComposedPanelCard(panelSpec, scope, onSettled) gained an optional third arg; slice d's poll-guard work and any future concurrency-budgeted caller can reuse the same seam.
  • I did not verify this against a live Add the alert-notebook binding endpoint (#4222) #4366 response body (no rig time in this lane); the shape is coded from the coordinator's message, not from reading Add the alert-notebook binding endpoint (#4222) #4366's diff directly.

pm-pr: merge order

Merge after #4366 (the endpoint) and #4367 (notebook read-cell validation). On dev today, ValidateNotebookDefinition accepts only markdown and panel cells, so "Save as notebook" would be rejected until #4367 lands.

pm-pr lane report (render fields, route guard, checks)

New head: ab1b1078 (was 9f5d59fc; merge commit 1a1631e5 brings in origin/dev, then fix commit ab1b1078). Pushed clean, no force.

1. Dropped fields (views.js renderAlertHeaderCell)

Added a "Value vs threshold" row ("current " + fmtNum(a.current_value,1) + " vs threshold " + fmtNum(a.threshold_value,1)), shown only when either field is a number — matches triage.js's alertFields formatting (fmtNum(_, 1)) so the two pages agree. Added a detail_text block using the exact same el("pre", { class: "code", text: a.detail_text }) triage.js uses, shown only when detail_text is truthy. Both fields go through el()'s { text: ... } prop (textContent); no innerHTML anywhere in the file. Null/absent fields are omitted, not rendered as blanks.

2. Route-change guard on alert-mode read cells

No existing AbortController/cancel pattern reaches alert-mode read cells (server.js's panelAbort/setPanelSignal is dashboard-only; views.js clears the signal for saved notebooks but alert mode never sets one). Added the smallest guard: triage.js's renderAlertNotebook captures isLive = () => location.hash === ourHash before its first await (same pattern renderView already uses for its own await gap), threads it through renderNotebookDoc's opts.isLive, and checks it at two more points: right after the /api/alert-notebook fetch, and after the getSession/getCatalog await. Inside views.js's per-cell loader, the fetch always runs and the limiter's slot always releases (so a superseded batch never starves the next page's reads); only the mount(holder, rendered) is skipped when isLive() is false. isLive is optional — saved-mode and any alert caller that omits it behave exactly as before (always live).

3. Checks

(a) Pins. Existing AlertNotebookRenderClientTests pins are source-text checks with no JS runner; all currently fail on origin/dev because neither file has any of this code there yet (grep for alert-notebook/mode: "alert"/isAlert/InFlightLimiter on dev returns 0 hits) — expected, since #4222/#4366/#4368 are unmerged slices. Added two new pins:

  • Views_AlertHeaderCell_RendersValueThresholdAndDetailText_ThroughElsTextPath — asserts a.current_value/a.threshold_value/a.detail_text appear, the exact el("pre", { class: "code", text: a.detail_text }) line, and DoesNotContain("innerHTML"). Fails on dev (file doesn't exist there yet / no such lines).
  • Views_AlertModeReadCells_GuardAgainstRenderingAfterARouteChange — asserts opts.isLive, const stillLive = !opts.isLive || opts.isLive();, if (stillLive) mount(holder, rendered); in views.js, and const isLive = () => location.hash === ourHash; plus isLive, in triage.js. Fails on dev, same reason.
    Both pins pass against this branch's committed source (checked by grep, since there's no JS runner here); the C# build is 0 Warning(s)/0 Error(s) on macOS (dotnet build Darling/Darling.Tests/Darling.Tests.csproj). Execution of the xunit suite itself requires Windows (net10.0-windows, WPF-adjacent deps) — not available on this host; CI decides pass/fail.

(b) CHANGELOG label. PR body currently has [#PR] / [#PR]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/PR. Needed text:

- **Alert-notebook read-only render on `#/triage`** ([#4368]) - ...
[#4368]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/4368

Not edited per orders (this lane makes no GitHub writes); coordinator/PR author should apply.

(c) CI state. gh-pm.sh pr checks 4368 reports "no checks reported on the 'feature/4222-alert-notebook-render' branch" immediately after this push (ab1b1078) — too fresh for CI to have started. Not waited on per orders.

4. Merge-order fact

Confirmed by reading origin/dev's DarlingWebEndpoints.cs::ValidateNotebookDefinition (~line 1868): the cell-type switch only has cases for "markdown" and "panel"; the default arm returns Fail($"cell {i} has an unknown type '{type}'; expected 'markdown' or 'panel'."). So "Save as notebook" POSTing a definition with "header"/"status"/"read" cells (this PR's shape) to /api/views is rejected by dev's validator today. #4367 (adding those cell types to the validator) must land before "Save as notebook" works end to end; #4368's own render path is unaffected (it never calls the validator).

Deferred

Nothing deferred — all four items and the CHANGELOG/CI checks were in-lane and completed within scope.

pm-pr lane report (CI pin fixes)

New head: b6b268df (merge commit; fix commit 646d9871 on top of ab1b1078, then git merge origin/dev — no conflicts).

Failure 1: innerHTML pin scoped to renderAlertHeaderCell

The old Assert.DoesNotContain("innerHTML", views, ...) ran over the whole views.js file, which also
contains a header comment ("...textContent (R4 — never innerHTML)."). That comment substring is what tripped
the pin, not any real innerHTML usage — renderAlertHeaderCell itself only ever uses el(..., { text: ... }).

Fix: extracted renderAlertHeaderCell's body via ReadRepoFileLf (LF-normalized, existing helper) by locating
"function renderAlertHeaderCell(" and slicing to the next "\nfunction " (the next top-level function,
saveAsNotebookButton). Asserted DoesNotContain("innerHTML", body) against that slice only. Kept the other
Contains assertions (current_value/threshold_value/detail_text, the el("pre", ...) text-path check)
unchanged.

Failure 2: (a) — updated the panels.js pin deliberately

git diff origin/dev...HEAD -- .../panels.js showed renderPanel(desc) → renderPanel(desc, onSettled) and a
new loadPanel(desc, body, signal, onSettled) wrapping the renamed loadPanelBody. This IS needed for this
slice: views.js's renderAlertCell calls renderPanel(cell, () => limiter.release()) to release the
alert-notebook's max-3 in-flight limiter (#4222's cost rule) when a panel's load settles. The route-change guard
(opts.isLive) is separate and lives entirely in views.js/triage.js — it does not touch loadPanel's
signature. So this is case (a), not a workaround to revert.

Updated WebFetchLayerTests.PanelsJs_CapturesAndAppliesTheRenderSignal to assert the new signatures
(renderPanel(desc, onSettled), loadPanel(desc, body, signal, onSettled), the matching call site), added a
comment citing #4368/#4222, and kept the render-signal behavioral assertions (setPanelSignal, the
aborted/auth kind check) unchanged since that behavior is untouched.

Other pins checked

Grepped all Darling.Tests/*.cs for views.js/triage.js/panels.js references. Only
RawWindowFloorViewerPortTests mentions loadPanel — in a doc comment only, no literal signature assertion —
safe. No other file pins renderNotebookDoc, renderPanel(, or loadPanel literally.

Verification

  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug: Build succeeded, 0 Warning(s), 0
    Error(s)
    — both before and after the git merge origin/dev (merge was clean, no conflicts, touched only
    DarlingWebEndpoints.cs/DarlingMcpBlockingTools.cs/DarlingServerResolver.cs, none of which this slice's
    files depend on).
  • Test project targets net10.0-windows (Darling.Tests is a Windows-only suite per repo convention) — cannot
    execute dotnet test/dotnet exec on this macOS worktree; CI is the actual gate for pass/fail, as ordered.
  • Pushed with a plain git push (no force). No rebase.

- panels.js renderPanel and compose.js renderComposedPanelCard take an
  optional onSettled callback, fired once a cell's load reaches a terminal
  state, so a caller with its own concurrency budget knows when a slot frees.
- views.js renderNotebookDoc is refactored to accept either a saved view
  (mode: saved, unchanged behaviour) or an in-memory definition bound to a
  firing (mode: alert, read-only): no fleet scope picker, a max-3 in-flight
  InFlightLimiter gating read-cell loads, header/status cells rendered
  directly from the endpoint's own facts, Save as notebook and Open live
  controls.
- triage.js tries GET /api/alert-notebook first and renders its definition
  through renderNotebookDoc in alert mode; on a 404 (endpoint not present on
  this build) it falls back to the existing /api/triage assembly unchanged.
- New pin tests (Darling.Tests/AlertNotebookRenderClientTests.cs) over the
  shipped JS source text: in-memory render path, no /api/fleet call in
  alert mode, the max-3 limiter, and the Save-as-notebook provenance string.

Built against PR #4366 (feature/4222-alert-notebook-endpoint, head
ee63879) for the /api/alert-notebook response shape.

Status refresh on the 60s poll is out of scope (needs slice d's poll guard
on dev). Not opening a PR yet per the coordinator's rule: pins exist and
build 0/0; a successor lane should verify the shape against #4366 once it
lands and open the PR.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 26, 2026 01:55
@erikdarlingdata
erikdarlingdata merged commit 2797849 into dev Sep 26, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/4222-alert-notebook-render branch September 26, 2026 01:56
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