Repository navigation
Alert notebook MVP: #/triage renders a read-only notebook bound to the firing (blocking and deadlocks authored, every other family converted) #4222
Description
Part of #4219.
Problem
The alert link lands on #/triage (wwwroot/js/pages/triage.js, backed by GET /api/triage in DarlingTriageEndpoint.cs). That page doesn't frame the firing:
Sections are generic. Each section is one /api/read result, auto-rendered through derive.js (triage.js:71-97). No chart marks the alert instant, and nothing tells the reader whether the condition has cleared.
The firing is matched by nearest time. The history match takes the nearest same-metric row (DarlingTriageEndpoint.cs:612-659). The link's dedup is only echoed as a notice (triage.js:151-153), although each history row's context_json carries its incidents' dedup keys (Mcp/DarlingAlertReader.cs:48-51). So the page can't name the incident, its database, or its objects.
Sections run serially, and the page re-runs every minute.
- Sections run one after another in a single request (
DarlingTriageEndpoint.cs:668-680). - The 60 s poll calls
route()(app.js:288-299), so an open triage tab re-runs the whole assembly every minute, though its anchored window can't change. AForced Plan Failingpage includes Query Store regressions, measured at 5–6 s per call on a heavy server (get_query_store_regressions' baseline is every retained row before the window (no lower bound): 5–9 s per call on a large store, the slowest read on the web Queries tab #4195).
The page can't become a notebook.
renderNotebookDocrenders only saved views (wwwroot/js/pages/views.js:487-540).ValidateNotebookDefinitionaccepts only composed panel cells (DarlingWebEndpoints.cs:1658, root keys:1335-1338). The forensic reads an investigation needs (blocking chains, deadlock graphs) are v1 reads, which dashboards accept (:1561-1587) and notebooks reject.
Fix shape
1. GET /api/alert-notebook?server&metric&at&dedup (after the auth middleware, like every sibling) returns:
{ alert, status, notes[], template: { id, version }, definition: { kind: "notebook", cells: [...] } }
alert: the history row whosecontext_jsonhas an incident with the link'sdedup. Without one, fall back to today's nearest same-metric row. Include the row's incident facts (Incident Since, Involved Objects, Database, Total Occurrences).status: "is it still happening":- a resolution row for this metric after
at(useResolutionAliases,DarlingTriageEndpoint.cs:99-135) reads "Resolved at T"; - otherwise, later firings of the same metric read "Fired again at …";
- otherwise the answer depends on collector freshness. If the relevant collector hasn't run since
at, the status is "unknown (not collected since T)", never "ongoing". An absence of rows is not evidence while the instrument is down.
- a resolution row for this metric after
definition: a template bound server-side.- Window:
window_end = min(at + 15 min, now)(today'sResolveAnchor), andwindow_start = min(Incident Since, at) − family lookback. - Composed cells get an absolute per-cell
range {windowStart, windowEnd}. Notebook panel cells share one view-level window — a two-window side-by-side isn't storable as live panels #2735 already allows it, and Notebook read view drops the #2735 "Pinned" time-window badge — a pinned cell silently shows a different window than the scope bar #2788 gives it the "Pinned" badge. - Read cells get
server,as_of = window_end, andhours = lookback. - Where a composed source has the dimension, the incident's database is bound as a variable value.
- Window:
2. A read cell type in notebooks: {type:"read", read, params, viz, title}.
- It's validated by the same v1 read-panel rules dashboards use (catalog read name, declared params).
renderCellroutes it torenderPanel.- Without it, "Save as notebook" fails on exactly the cells that matter.
3. Templates live server-side, as the evolution of SectionsByMetric.
- Authored for the MVP:
- Blocking Detected / Blocking Wait Time:
- header and status cells;
get_blocking(limit 20);- a
blocked_process_reportstimeline with a deadlock annotation; - most-blocked objects, by database, and lock modes, reusing the
blocking-rcapanel specs; get_active_queries blocking_only.
- Deadlocks Detected:
- header and status cells;
get_deadlock_detail(limit 3);- the deadlocks timeline with a blocking annotation;
- deadlocks by database, reusing the
deadlock-postmortemspecs; get_deadlock_trend24 h.
- Blocking Detected / Blocking Wait Time:
- Every other metric converts mechanically: its current
SectionsByMetriclist (or fallback) becomes read cells under the same header and status cells. Every family gets the notebook shell, the dedup match and status from day one. - Each template carries an
idandversion.
4. #/triage renders the alert notebook through renderNotebookDoc, refactored to take an in-memory definition.
- Read-only.
- No fleet scope picker: the server is fixed, so no
/api/fleetread (Web viewer: every server-tab switch re-downloads the whole fleet roll-up (/api/fleet, 66–81 KB) to render one server's header card #4190). - "Save as notebook" (can_edit seats) POSTs the bound definition to
/api/views. The provenance "from alert template v, on at " goes in the description, since the definition's root keys are closed. - "Open live" drops the window pins so the view-level relative scope takes over.
- Explicit
vizper cell. Noderive.jsguessing. /api/triagestays one release for anything scripted against it, then goes.
5. Cost.
- Cells load client-side through
/api/readand/api/compose/run, top to bottom, at most 3 in flight. That replaces the serial server-side assembly, and cells pass the render's AbortController signal (Web viewer: the 60 s poll and tab switches re-fire a page's reads while the previous render's reads are still running, and superseded reads are never cancelled #4191). - The triage route joins
app.js's poll guard, so it doesn't re-render every 60 s. Only the status cell refreshes: one history read and one freshness read. - Template rules, enforced by pins:
- every read cell has an explicit
limit, or its trend goes through the chart bucket budget; - every window is at or under the family lookback;
- no
audit_config, and noanalyze_*; get_query_store_regressions, when present, is a collapsed click-to-run cell.
- every read cell has an explicit
- Nothing is computed or stored at fire time, as today.
Where
Darling/PerformanceMonitor.Darling.Service/DarlingTriageEndpoint.cs: map ~:167-334, fallback ~:381-385,ResolveAnchor~:531-554, history match ~:612-659DarlingWebEndpoints.cs:ValidateNotebookDefinition~:1658, root keys ~:1335-1338, per-cell range ~:1762- dashboard read-panel rules ~:1561-1587
/api/compose/runabsolute window ~:991-1034- route map ~:322-324
wwwroot/js/pages/triage.js,wwwroot/js/pages/views.js(renderNotebookDoc~:487,renderCell,panelOrError~:744-767),wwwroot/js/notebook.js(blocking-rca~:516,deadlock-postmortem~:583),wwwroot/js/app.js(route ~:63-69,refresh~:288-299)Mcp/DarlingAlertReader.cs(context_jsonon history rows)
Pins
- Every shipped alert template, bound against a fixed alert, passes
ValidateNotebookDefinition. A renamed measure or read reds CI. - Every read cell names a read in
BuildReadDispatch. This extends the existing map↔dispatch pin. - Budget pin over every template: limits and windows within caps, no barred reads, and Query Store regressions only as click-to-run.
- Dedup match: among two same-metric rows 30 s apart with different incident dedup keys, the link's
dedupselects the right row even when the other is nearer in time. With nodedup, today's nearest-row behaviour holds. - Status arms: resolution row after
atgives resolved; later same-metric row gives fired again; neither plus a fresh collector gives no resolution recorded; neither plus a collector with no run sinceatgives unknown. The last one must not read "ongoing". - Binding: composed cells carry an absolute
rangeequal to the computed window; read cells carryas_of = window_end. Anatin the future clamps to now, asResolveAnchordoes today. - Notebook
readcell: the validator accepts a valid read cell, rejects an unknown read or an undeclared param by cell index, and a saved notebook with read cells round-trips through/api/views. - Poll guard:
refresh()doesn't callroute()on the triage route. Copy the editor-route guard's pin shape. - Unauthenticated
GET /api/alert-notebookreturns the login response (or Web viewer: after a service restart, every open tab's API reads come back as the 200 HTML login page, which the page classifies as empty data #4187's 401) and issues zero store commands, on the Darling: a live HTTP test for both hosts' gates, including /core #4128 live-HTTP pipeline with a counting data source.
Out of scope
Authored templates for other families (slice D), link delivery (A), sign-in (B), user-editable override templates, and an MCP read returning the bound notebook.
Activity
Add pins for the alert-notebook endpoint (#4222 slice C, security-first)
Add a shell-level auto-refresh pause/resume control (#4222) (#4365)
Add a live round-trip pin for a notebook read cell through the custom…
erikdarlingdata commentedon Sep 26, 2026
Claude posting for Erik Darling
Slice (d), the web viewer's shell-level pause/resume control for the 60 s auto-refresh (the triage page no longer re-renders on every tick): #4365, merged to dev (420033e). The alert-notebook endpoint (#4366), the notebook read cell (#4367) and the triage render (#4368) are still open, so this issue stays open.
Remove the unused windowStart computation in the alert-notebook endpo…
Notebook read cell type, validated by the shared v1 read-panel rules (#…
erikdarlingdata commentedon Sep 26, 2026
Claude posting for Erik Darling
Slice (a2), the notebook
readcell type (validated by the same rules as a dashboard's v1 read panel; those shared rules now also check a read panel'sparamson save, for dashboards too): #4367, merged to dev (ff0373b). The alert-notebook endpoint (#4366) and the triage render (#4368) are still open, so this issue stays open.Add the alert-notebook binding endpoint (#4222) (#4366)
erikdarlingdata commentedon Sep 26, 2026
Claude posting for Erik Darling
Slice (a),
GET /api/alert-notebook(an alert turned into a mechanical notebook: the dedup match nearest the alert, four status arms with a forward read for later resolutions, window binding, and read cells naming only dispatch reads): #4366, merged to dev (9aa1519). The triage render (#4368) is still open, so this issue stays open.Alert notebook: read-only render on #/triage (#4222) (#4368)
erikdarlingdata commentedon Sep 26, 2026
Claude posting for Erik Darling
Slice (c),
#/triagerenders the alert notebook read-only (current vs threshold and detail text in the header, at most 3 cells loading at once, results dropped after a route change, falling back to the old triage view on a 404): #4368, merged to dev (2797849). Slices (a), (a2), (c) and (d) are now on dev; the authored templates (b) remain, so this issue stays open.One summary per member (#4222)
Add the authored Blocking and Deadlocks alert-notebook templates (#4395)
erikdarlingdata commentedon Sep 26, 2026
Claude posting for Erik Darling.
#4395 is merged. Blocking Detected, Blocking Wait Time and Deadlocks Detected now open an authored alert notebook: a timeline, ranked breakdowns and drill-down reads, bound to the alert's window and database. Every other alert type keeps the mechanical layout.
erikdarlingdata commentedon Sep 26, 2026
Claude posting for Erik Darling
Closing: the alert-notebook MVP has shipped.
GET /api/alert-notebook, the firing turned into a notebook, with the dedup match, the status arms and the window binding.readcell type.#/triagerenders the notebook read-only.Follow-ups: