Repository navigation
Alert notebook: read-only render on #/triage (#4222) - #4368
Merged
Merged
Conversation
- 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
marked this pull request as ready for review
September 26, 2026 01:55
This was referenced Sep 26, 2026
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.
Refs #4222.
Why
Slice C of the alert-notebook MVP: the client render side.
#/triageneeds to render a read-only notebook document bound to one firing, sourced fromGET /api/alert-notebook(PR #4366), while the saved-view notebook renderer keeps working unchanged.What changes
wwwroot/js/pages/views.js:renderNotebookDocis 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/fleetread), aheader/statuscell pair rendered directly from the endpoint's own facts (no read for those two), aread-type cell rendered through the sharedrenderPanel, a max-3 in-flightInFlightLimitergating those read-cell loads, and "Save as notebook" / "Open live" controls.wwwroot/js/pages/triage.js: triesGET /api/alert-notebookfirst; on success renders itsdefinitionthroughrenderNotebookDocin alert mode, showingalert,status, andnotes[]. On a 404 (endpoint not on this build yet) it falls back to the existing/api/triageassembly, unchanged.wwwroot/js/panels.js(renderPanel) andwwwroot/js/compose.js(renderComposedPanelCard): both now take an optionalonSettledcallback, 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 inViewTemplatesTests/ServerPageTabsTests).Built against PR #4366 (
feature/4222-alert-notebook-endpoint, headee6387936) for the/api/alert-notebookresponse shape:{ alert, status, notes[], template: {id, version}, definition: {kind:"notebook", cells:[...]} }, withcells[0]aheadercell,cells[1]astatuscell, andcells[2..]readcells. That shape has no composed cells and no per-cellrangeyet, so "Open live" is implemented as dropping each read cell'sas_ofparam.Out of scope, by design: status refresh on the 60s poll (needs slice d's poll guard on
app.js'srefresh(), not landed); templates for other alert families (slice B in the umbrella design); thereadcell type's server-side validation andrenderCellrouting (slice a2, not landed — this PR calls the sharedrenderPaneldirectly in alert mode rather than through the dashboard's genericrenderCell, since that routing doesn't exist yet ondev).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 --checkon all four touched JS files, plus a Node ESM import resolution smoke check onviews.jsandtriage.js(no missing-export/import errors)AlertNotebookRenderClientTests(new) — builds; CI decidesCHANGELOG entry
SECTION: Added
ENTRY:
#/triage([Alert notebook: read-only render on #/triage (#4222) #4368]) - the alert deep link can now render the bound notebook document returned by/api/alert-notebook(server, status, and forensic read cells for the firing), read-only with no fleet scope picker; falls back to the existing triage assembly when the endpoint isn't present.REF:
[Alert notebook: read-only render on #/triage (#4222) #4368]: Alert notebook: read-only render on #/triage (#4222) #4368
For the coordinator
feature/4222-alert-notebook-render, head9f5d59fce) but built against Add the alert-notebook binding endpoint (#4222) #4366 while it was still unmerged. Re-verify the/api/alert-notebookresponse 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 newoptsobject shape ({mode, definition, alert, status, notes, canEdit, catalog, provenance, onOpenLive}for alert mode) is exported fromviews.js— slice a2 (the genericread-cellrenderCellrouting) can reuse it onceValidateNotebookDefinitionaccepts read cells ondev; today this PR bypassesrenderCelland callsrenderPaneldirectly 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)andcompose.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.pm-pr: merge order
Merge after #4366 (the endpoint) and #4367 (notebook read-cell validation). On dev today,
ValidateNotebookDefinitionaccepts onlymarkdownandpanelcells, so "Save as notebook" would be rejected until #4367 lands.pm-pr lane report (render fields, route guard, checks)
New head:
ab1b1078(was9f5d59fc; merge commit1a1631e5brings inorigin/dev, then fix commitab1b1078). 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 — matchestriage.js'salertFieldsformatting (fmtNum(_, 1)) so the two pages agree. Added adetail_textblock using the exact sameel("pre", { class: "code", text: a.detail_text })triage.js uses, shown only whendetail_textis truthy. Both fields go throughel()'s{ text: ... }prop (textContent); noinnerHTMLanywhere 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/setPanelSignalis dashboard-only; views.js clears the signal for saved notebooks but alert mode never sets one). Added the smallest guard:triage.js'srenderAlertNotebookcapturesisLive = () => location.hash === ourHashbefore its first await (same patternrenderViewalready uses for its own await gap), threads it throughrenderNotebookDoc'sopts.isLive, and checks it at two more points: right after the/api/alert-notebookfetch, and after thegetSession/getCatalogawait. Insideviews.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 themount(holder, rendered)is skipped whenisLive()is false.isLiveis optional — saved-mode and any alert caller that omits it behave exactly as before (always live).3. Checks
(a) Pins. Existing
AlertNotebookRenderClientTestspins are source-text checks with no JS runner; all currently fail onorigin/devbecause neither file has any of this code there yet (grepforalert-notebook/mode: "alert"/isAlert/InFlightLimiteron dev returns 0 hits) — expected, since #4222/#4366/#4368 are unmerged slices. Added two new pins:Views_AlertHeaderCell_RendersValueThresholdAndDetailText_ThroughElsTextPath— assertsa.current_value/a.threshold_value/a.detail_textappear, the exactel("pre", { class: "code", text: a.detail_text })line, andDoesNotContain("innerHTML"). Fails on dev (file doesn't exist there yet / no such lines).Views_AlertModeReadCells_GuardAgainstRenderingAfterARouteChange— assertsopts.isLive,const stillLive = !opts.isLive || opts.isLive();,if (stillLive) mount(holder, rendered);in views.js, andconst isLive = () => location.hash === ourHash;plusisLive,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:Not edited per orders (this lane makes no GitHub writes); coordinator/PR author should apply.
(c) CI state.
gh-pm.sh pr checks 4368reports "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'sDarlingWebEndpoints.cs::ValidateNotebookDefinition(~line 1868): the cell-typeswitchonly has cases for"markdown"and"panel"; thedefaultarm returnsFail($"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/viewsis 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 commit646d9871on top ofab1b1078, thengit merge origin/dev— no conflicts).Failure 1: innerHTML pin scoped to renderAlertHeaderCell
The old
Assert.DoesNotContain("innerHTML", views, ...)ran over the wholeviews.jsfile, which alsocontains a header comment ("...textContent (R4 — never innerHTML)."). That comment substring is what tripped
the pin, not any real
innerHTMLusage —renderAlertHeaderCellitself only ever usesel(..., { text: ... }).Fix: extracted
renderAlertHeaderCell's body viaReadRepoFileLf(LF-normalized, existing helper) by locating"function renderAlertHeaderCell("and slicing to the next"\nfunction "(the next top-level function,saveAsNotebookButton). AssertedDoesNotContain("innerHTML", body)against that slice only. Kept the otherContainsassertions (current_value/threshold_value/detail_text, theel("pre", ...)text-path check)unchanged.
Failure 2: (a) — updated the panels.js pin deliberately
git diff origin/dev...HEAD -- .../panels.jsshowedrenderPanel(desc)→renderPanel(desc, onSettled)and anew
loadPanel(desc, body, signal, onSettled)wrapping the renamedloadPanelBody. This IS needed for thisslice:
views.js'srenderAlertCellcallsrenderPanel(cell, () => limiter.release())to release thealert-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 inviews.js/triage.js— it does not touchloadPanel'ssignature. So this is case (a), not a workaround to revert.
Updated
WebFetchLayerTests.PanelsJs_CapturesAndAppliesTheRenderSignalto assert the new signatures(
renderPanel(desc, onSettled),loadPanel(desc, body, signal, onSettled), the matching call site), added acomment citing #4368/#4222, and kept the render-signal behavioral assertions (
setPanelSignal, theaborted/auth kind check) unchanged since that behavior is untouched.
Other pins checked
Grepped all
Darling.Tests/*.csforviews.js/triage.js/panels.jsreferences. OnlyRawWindowFloorViewerPortTestsmentionsloadPanel— in a doc comment only, no literal signature assertion —safe. No other file pins
renderNotebookDoc,renderPanel(, orloadPanelliterally.Verification
dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug: Build succeeded, 0 Warning(s), 0Error(s) — both before and after the
git merge origin/dev(merge was clean, no conflicts, touched onlyDarlingWebEndpoints.cs/DarlingMcpBlockingTools.cs/DarlingServerResolver.cs, none of which this slice'sfiles depend on).
net10.0-windows(Darling.Tests is a Windows-only suite per repo convention) — cannotexecute
dotnet test/dotnet execon this macOS worktree; CI is the actual gate for pass/fail, as ordered.git push(no force). No rebase.