Skip to content

Release 3.10.0 - #5645

Merged
erikdarlingdata merged 399 commits into
mainfrom
dev
Oct 9, 2026
Merged

erikdarlingdata merged 399 commits into
mainfrom
dev

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Promotes dev to main for the 3.10.0 release. Full entries: docs/changelog/3.10.md (indexed in CHANGELOG.md), dated 2026-10-09. The head is dev.

What 3.10.0 is

The release has 109 Added, 42 Changed and 142 Fixed entries. Most Added entries are for the Darling web dashboard. It now shows much of what the desktop viewer shows. The rest of the release makes the Darling store cheaper to run on a large fleet.

Headline changes:

  • Web dashboard. The FinOps page has its full set of tabs. There is an Admin page, a Job History page, a Mute Rules page and server tag management. Grids sort, filter by column and export. Charts zoom, show a menu and hide series. Stored plans, deadlock graphs and a repro script open from the grids.
  • One time range picker for Lite, the Darling viewer and the web pages, with calendar periods and typed ranges such as last 3 days. Text column filters list their values to tick and stay set across refreshes and restarts.
  • MCP. New tools and views include get_job_history, edit_server, server trends, FinOps views and the repro script. The listeners can serve HTTPS and take a list of allowed ranges.
  • Alerting. PagerDuty can keep an outage and its recovery in one incident. Darling can ping a dead-man's-switch URL while it is collecting. It warns about collection gaps at start and in the stored history. A Linux service can now keep saved passwords and webhook secrets.
  • Darling store. Query Store interval tables are partitioned by day. Perfmon data is compressed per counter. Hourly rollups serve Top Queries and Top Procedures. Retention gives way to collection. The hourly maintenance step over stored analysis findings no longer times out on a large store.
  • Lite. A DuckDB memory limit setting, and a Job History tab that reads only the selected range.
  • Upgrades. An upgrade over an existing install no longer lists the service's own files when it refuses, and a refusal comes with numbered steps.

Before upgrading

The release's Important section applies: upgrading from 3.9 runs the Darling store migrations V158 through V173 on the service's first start. They are almost all new tables that start empty, plus a few small column additions. None of them rewrites a large table or needs an offline window.

Store and schema

Dev is at schema 173. Nothing in install/ or upgrades/ changed since v3.9.0, so the deprecated SQL Server installer scripts are untouched. The store migrations live in the Darling service and run when it starts.

Checklist status

  • Done: steps 1, 3 and 9 (version bumps, the schema audit and the nightly check).
  • Done: step 2 (CHANGELOG), 2a (archive and compact) and 2b (README sync), in Release 3.10.0: last changelog splice and README sync #5635. Step 2a ran last, after the last PRs for this release merged.
  • Step 4 (build and statement census): the dev push build shows 0 warnings and 0 errors in every project it builds. A Debug build of the whole solution at the same commit, the deprecated Dashboard and Installer included, also shows 0 warnings and 0 errors. The statement census has no pending entries.
  • Step 8a (Darling service install and upgrade): on Windows, a fresh install and an upgrade from 3.9.0 pass on both runner images. So does an upgrade from 3.3.0, the oldest supported release. The 3.3.0 install sits directly under C:\, so the upgrade stops once and prints the move steps, as the Important entry says. Following them finishes the upgrade. On Linux, the tarball and the container image each pass a fresh install and an upgrade from 3.9.0. They also pass an upgrade from 3.4.0, the oldest release that shipped them. Every finished leg ends at schema 173 and collects with no failed collector runs. The builds tested differ from The pre-lock check trusts the install's own service SID and an existing install gets exact steps (#5627) #5628's final commit only in one test file and in the check's own workflow.
  • Step 5 (cloud platform testing): an Azure SQL Database run on the current dev commit is in progress. AWS RDS is covered by a monitored fleet that is all RDS.
  • Steps 6 and 7 (Lite collectors, new features): we walked Lite, the web dashboard and the Darling viewer on 10-07 and 10-08. The fixes from those walks are in this release (Release checks: small fixes from the final walk of Lite and the Darling Viewer #5542). The security changes in Darling web: response security headers; clearer consent text; connection-test errors carry less detail; token honored on loopback listeners #5636 were checked on a running service at its merge commit.
  • Step 8 (desktop upgrade): the upgrade code is unchanged since v3.9.0. The Setup.exe upgrade over 3.9.0 is checked on a clean machine after publishing.
  • Left: steps 10 and 11 (this PR, tag and release).

Darling web and Viewer hardening (#5636): the web host sends response security headers, and the consent text before re-running a captured query is clearer. Connection-test errors carry less detail, and a configured token is honored on loopback-only listeners.

This merges after the Azure SQL Database run passes.

…y Store clutter tiles say where their data starts (part of #4966) (#5144)

The web server page's Memory Grant Pressure, Resource Semaphore Pressure, Plan Corrections and Query Store clutter panels now say where their data starts, so a server with a short history no longer shows a 7-day range as if it were all quiet.

The server adds the note for get_memory_grants and get_resource_semaphore (both probe memory_grant_stats, the table their window aggregates read) and get_plan_corrections (plan_correction). Plan Corrections is a newest-first list, so a capped page names the oldest recommendation it returns, and its "empty" answer also gets the note. The page's shared note strip learns three things: a spec can opt out (windowNote: false), which the newest-snapshot panels use (the two grants lists and Automatic Tuning); a stat tile can draw it as well as a table; and a spec can read the note from a nested block (floorKey), which the Query Store Clutter and Query Store Overhead panels use for the note their tool already returns. The shared fanout's empty path now draws the note as the single-panel path does.

Tests: page scenarios through the shipped util.js, panels.js and server-tabs.js in the Node harness (the opt-out, a stat, the nested note, the fanout's snapshot sibling, an empty Plan Corrections answer with and without the flag, Automatic Tuning quiet); the capped-rule tests extended to Plan Corrections; a live case for a memory read whose rows start inside the window; the admitted status words.

Part of #4966
… tables (rung V161) and the hourly builder (#5094) (#5125)

Daily Query Store summary for get_query_store_top's long windows: the tables (rung V161) and the hourly builder (#5094)

Part of #5094. No reader uses the summary yet; the read and its "approximate" flag come next.

- Rung V161 adds two plain tables. collect.query_store_top_daily holds one row per server, UTC day and query identity (database, query id, plan id, query hash, outcome, replica role, module), with sums and non-NULL counts that recombine exactly, so a reader reproduces today's unweighted mean of interval averages. Its unique key is NULLS NOT DISTINCT. collect.query_store_top_daily_built records each built day's pass. The viewer's schema probe gains the V161 sentinel and arm.
- QueryStoreTopDaily is the hourly store-maintenance tick's fifth tenant. Pass 1 builds a day at its end + 2 h, pass 2 rebuilds it at its end + 25 h, and a day first seen past that point is built once at pass 2. Each build is one transaction under a 60 s statement timeout, bounded on first_execution_time to the day (−26 h, +1 h), for enabled servers and days after their coverage claim, oldest first. The tick does at most 60 builds and starts none after 10 minutes; GC follows the interval table's 9-day horizon and disabled servers.
- Approximate by design (Erik's call on #5094): a late write into a built day is missed, a row moving into the next day after pass 1 counts twice until pass 2, and a monitored server clock more than an hour ahead drops rows past the day's end + 1 h. Each case is pinned by a live test.
- On a production store, one build of the largest server's day read about 197k interval rows (30k blocks) into 26k groups in 2.3 s.
The web dashboard gains a Manage Tags page, over the server-tag write routes (#5137). It reads the tag forest and each server's tags from the existing `/api/fleet`, so it adds no route, grant or schema change.

**What the page does.**
- **The tree:** up to four levels, with each tag's colour and its server count from the payload.
- **Tag actions:** create a root or child tag, rename, recolour, move under another parent, and delete.
- **Delete:** the page first asks the server without confirming. If deleting would leave custom alert rules matching no server, it names those rules and asks again, then sends `?confirm=true`.
- **Assignment:** a server checklist assigns the selected tag. The page records only the boxes the user changed, so Apply sends one assign for the added servers and one unassign for the removed ones, and never touches a server someone else changed meanwhile.
- **After each write:** the page lists the alert rules whose coverage changed.
- **The 60-second refresh** keeps the selection, the unsaved checkbox changes (marked "(unsaved)") and the open form, including the focused input.
- **In-flight writes:** while a write is running, a second click sends nothing, and Cancel and the form openers wait for it.
- **A parent deleted elsewhere** keeps the form and what was typed, and resets the parent to none.
- **Read-only sign-ins** see the page with every write disabled.

Every server string reaches the page through the house `el()` helper as text. A colour is used in a style only after it matches `#RRGGBB`.

**Tests.** The behaviour tests drive the page under Node with a fake fetch, and check each request's method, path, content type and body:
- the tree and the forms;
- the delete confirm flow;
- the assign and unassign diff, including servers changed elsewhere;
- the refresh keeping the form and the edits;
- double-click guards;
- 403, 404, 409, 415, 500 and network refusals;
- read-only.

Page tests pin the routes and methods.

Part of #5085
)

`get_read_latency` returned up to `limit` rows (at most 1,000) and never fitted its response to the shared MCP response budget (`McpResponseBudget.DefaultBytes`, 32,768 bytes). With 1,000 routes, a `limit=1000` answer was 227,445 bytes.

The tool now builds its response the way the availability-group reader does:
- it takes rows in the total order (p95 descending, then count, then surface and route) while they fit;
- it then serializes the real final response, dropping the last row and re-serializing while it is still over the budget.

The answer can therefore never exceed the budget, even by the few bytes the counters add.

**New fields.** The response carries `truncated` (the limit or the budget cut the page), `reads_returned` and `reads_total`. Rows always drop from the end of the order. The guide tail says the page is capped at `limit` rows or the budget, whichever cuts first, and to raise `limit` or narrow with `surface` or `route`. The tool head is unchanged. The web read route returns the same capped answer.

**Tests.**
- A live test seeds 1,000 routes, then asserts the size bound, the three fields, and that the rows returned are the top of the order.
- A unit test sweeps the budget across the last row's edge, and asserts the UTF-8 size never exceeds it.

Fixes #5135
…ction log, plan correction and memory pressure tools, say where their data starts (part of #4966) (#5147)

This lands two changes for #4966: the Darling MCP twins of Lite's data-start tools (first reviewed as #5131), and Lite's config-change, collection-log, plan-correction and memory-pressure tools.

Darling's get_active_queries, get_waiting_tasks, get_query_heatmap and get_query_store_regressions now say where their data starts, with the contract Lite's twins use (#5015): effective_start (UTC, with a Z), window_truncated and truncation_note right after hours_back on a data answer, and the same three keys under hints on an empty answer, so an empty or short answer over a window the store never covered no longer reads as a quiet one. not_collected and unavailable answers keep their shape. get_waiting_tasks, the heatmap and the regressions tool always carry the keys, as Darling's Queries tools do; get_active_queries carries them only when the window was cut, by the house rule that a payload omits null and false fields.

A shared helper, DarlingMcpWindowNotice, probes coverage through DataWindowFloor.GetAsync (the registry's created_date and the purge edge, a run counting as well as a row), with Lite's sentences copied and pinned. A window of 90 minutes or less with rows starts no probe; an empty answer always probes; the regressions tool probes from its baseline start. A probe that fails costs the notice, never the rows: it is logged and the answer goes without the keys. The web keeps its own note for the reads it lists (WebDataStartNote strips the tool's keys before deciding); the Active Queries, Query Heatmap and Query Store Regressions panels now show the tool's own sentence, as the Top Queries panels already do.

In both apps, get_active_queries now previews query text at 400 characters instead of 500, so a cut window (keys plus note) stays inside the 32,768-byte response budget: the default call over the budget test's 50 heavy rows is 30,223 bytes covered and 30,504 cut. get_active_queries' and get_waiting_tasks' descriptions now name the three keys in both apps.

Tests: helper tests (the 90-minute rule, the null floor, the exact sentences, a probe that throws, a caller who cancels); live tests for each tool (a notice when coverage starts inside the window, none on a quiet start, hints on an empty answer, a failed probe that keeps the rows); a covered and a cut budget case; the web guard; the census rosters and the shared-formatter pin. The full Darling suite fails only tests on the Mac-only list; Lite's description, budget and guide classes pass on Windows.

Lite's get_server_config_changes, get_database_config_changes, get_trace_flag_changes, get_collection_log (one server), get_plan_corrections and get_memory_pressure_events now say where their data starts, with the contract the other Lite window-floor tools use (#5004, #5015, #5127): effective_start (UTC, with a Z), window_truncated and truncation_note right after hours_back on a data answer, the same keys under hints on an empty answer, and not_collected and unavailable answers unchanged. The fleet form of get_collection_log names no server, so it carries no notice.

Each probe reads the source its tool returns rows from: the config snapshots on capture_time with their collector's runs, the collection log over the read's own window, v_plan_correction, and memory pressure on sample_time. None needs the event-time rule, because each probe already reads the column the rows are windowed on. The config change lists diff each in-window snapshot against the one before it, so a snapshot from before the window also counts as coverage, but only when the server was monitored during the window (at least one collection-log run); with no run in the window the answer says nothing was read (LocalDataService.GetConfigSnapshotCoverageFloorAsync). A plan-corrections answer that holds only automatic-tuning rows is probed as a data answer. The one-server collection log now treats a status filter as a filter, so "no ERROR runs" takes the filtered-empty answer rather than the quiet-window one.

Tests: McpWindowNoticeConfigAndLogToolTests (new): for each tool, a notice when collection starts inside the window, none on a quiet start, hints on an empty answer past coverage, the key order, a short window that starts no probe, a capped page where the tool pages; the config tools' pre-window snapshot with and without a run in the window; a status filter that matches nothing; a one-hour automatic-tuning-only answer. The census rosters and the shared-formatter pin cover the four files. On Windows, the three Lite window-notice classes pass (147).

Part of #4966
…nditions and Node OOM (#5152)

Part of #4843

## What users see change
- The System Events **Memory Conditions** (32 columns) and **Memory Node OOM** (28 columns) tables now open narrow. A "Columns:" strip above the table has one toggle per column group, and a pressed toggle shows its columns.
  - Memory Conditions: Event Time, Last Notification, Report and the OOM counters are always shown. Groups: Process memory (on at first), Pressure flags, Allocation detail.
  - Node OOM: Event Time and the node ids and utilisation are always shown. Groups: Failure (on at first), Memory sizes, Pool detail, Flags.
- The choice survives the 60 s repaint. Another server's table starts from the defaults.
- Every other grid is unchanged.

## What changed
- `panels.js`: `vizTable` takes `groups` and `defaultGroups`. A column carries `group: "<name>"`; a column with no group is always shown. The toggle state sits in a module-scope map, keyed like the grid sort state (route, table identity, columns). A hidden column is still built and only set to `display: none`, so the sort, Copy and CSV column indexes stay aligned.
- `server-tabs.js`: `table()` takes an optional last argument `columnGroups = { groups, defaultGroups }`. The two wide column constants are grouped in the desktop grid's order, and #5118's "picker candidate" comments are gone.
- `css/app.css`: `.col-picker` rules beside the `.grid-tools` rules.
- No new module, so no harness needed an import change.

## Decisions
- **Copy and CSV export all columns, hidden ones included.** The desktop grid's export (`DataGridExport`) iterates every column of the grid, shown or not, so the web matches it. Copy cell and Copy row follow the same rule.
- **Sort:** hiding a sorted column keeps the sort. The order holds, the repaint re-applies it, and the other columns still sort.

## What's not changed
- No other grid gets groups. The wider Top Queries and Query Store grids are for the following lanes, which can reuse this option.

## Verified at the head
- New `ColumnGroupsBehaviourTests` (node, 6 tests): no groups gives an unchanged table, defaults, toggling, rebuild and server switch, sort with a hidden column, and Copy carrying all columns. RED on dev: with dev's `panels.js` four scenarios crash with no picker and the fifth shows no strip. 6/6 green now.
  - The no-groups case asserts the DOM is dev's: children are exactly `grid-tools`, `table-wrap`; no th or td carries a `display` style; `tools:false` returns the bare `table-wrap`.
  - The sort case uses a fixture where A's order (y, x, z) differs from B's (y, z, x). With the comparator forced to return 0, the check fails (`byA` comes back x, y, z).
- New `ServerTabColumnGroupsPinTests` (2): each grid's groups match its group list and defaults, and each constant is the last argument of its own `table()` call (Memory Conditions, Memory Node OOM). 2/2.
- `ServerPageTabsTests` 19/19, `WebModuleLoadTests` 7/7, `GridToolsBehaviourTests` 9/9, `PgIndexBloatCoverageTests` 16/16 (this file is not changed by the PR; its `span, noteKey }` pin still matches because the `table()` literal keeps that text).
- The wider guard family and every `.mjs`-harness class: 239 classes run on this Mac. The one failure, `LivePostgresCollectionHygieneTests`, is in `dev-local-failing-tests.txt`.
- `ServerPageTabsTests` was updated: it now lists `columnGroups = null` in the `table()` signature.
- `scrubcheck` on the diff exits 0.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Column toggles on the wide System Events grids** ([#5152]) - Memory Conditions and Memory Node OOM open with a short default column set and a "Columns:" strip that reveals the rest, and the choice survives the page's refresh.
REF: [#5152]: #5152
)

Part of #4843

## What users see
On the web Alert History page, a signed-in seat that can edit now gets:
- a checkbox on every live row;
- a **Dismiss Selected** button (it shows the checked count);
- a **Dismiss All** button that dismisses every live row the current filter box shows, after the Darling Viewer's prompt ("Dismiss N alert(s)? … hidden from this view but remain in the store"). When the page is truncated (more alerts than the row limit shows), the prompt adds that only the listed alerts are dismissed and the rest stay live.

After a dismiss the list refreshes. With Show dismissed off the rows disappear. With it on they stay and say "Dismissed". A short status line reports the counts, for example "Dismissed 12 of 12", or "Dismissed 1 of 4; 3 were already gone". A refusal (403, 401, 400, 5xx) shows the server's text in the status line and the page never claims success. A read-only seat sees none of this.

## What changed
- `alerts.js`: the select column, the dismiss bar and the write, which goes through the shared `apiSend` helper (JSON content type, session-expiry handling) to `POST /api/alert-history/dismiss` (#5106). The body is `{alerts:[{alert_time, server_id, metric_name}]}`.
- The checked rows live at module scope, keyed by the row key. They survive the 60 s rebuild and a visit elsewhere and back. After a successful dismiss only the keys that were sent are removed, so a box ticked while the request was in flight stays checked. A failed or empty poll read keeps the set; only a successful read prunes rows that left the list. The set is cleared when the range, row limit, server, Show dismissed or filter box changes. 
- Dismiss All sends the listed rows, with no new route. This differs from the Viewer, whose Dismiss All covers every live alert in the window, even past the row limit; the web keeps its listed-rows design and says so in the prompt when the page is truncated. The route caps a request at 1000 keys, so a longer list goes in chunks of 1000 and the counts are added up. A failure part-way says how far it got and refreshes the list.
- Dismissed rows, and rows with no `server_id`, get no checkbox and are not sent.
- `panels.js`: a column flag `copy: false` leaves a column out of Copy row and Copy all (the CSV already honours `csv: false`). The Select column sets it; the Mute column is unchanged. Copy cell and the picked-cell lookup still see every column.
- `css/app.css`: three rules beside `.filter-box`.
- The #5109 range controls and the #5110 `markDismissed` / `tools: false` row build are untouched.

## What's not changed
No server code, no other page, `util.js` or `server-tabs.js`; in `panels.js` only the `copy` flag. There is no "since" route.

## Tests
- New `AlertHistoryDismissBehaviourTests` (13, with `alert-history-dismiss-harness.mjs`, run under Node on the shipped page): no controls for a read-only seat; Dismiss Selected posts the right keys and refreshes; Show dismissed keeps the row with "Dismissed"; Dismiss All posts what the filter shows; chunking at 1000; the checked set surviving a poll and a re-open and clearing on a filter change; a 403 and a 401 shown and the selection kept; the counts line; the prompt wording and the truncated-page line; Copy row and Copy all without the Select column and with Mute kept; a failed poll read keeping the checked set; a box ticked mid-dismiss surviving unsent.
- `AlertHistoryRangePageTests` dropped its "page has no Dismiss Selected" assertion. `MuteRulesPageTests` pins the new column expression. The range harness's metric column index moved from 2 to 3 because of the select column.

## Verified at the head
- RED on dev: with `alerts.js` reset to dev, 8 of the original 9 new tests fail; the four added for the prompt, Copy and selection handling also fail there. The read-only test passes there because it has nothing to show.
- GREEN: `AlertHistoryDismissBehaviourTests` 13/13, `AlertHistoryRangePageTests` 12/12, `MuteRulesPageTests` 6/6, `WebModuleLoadTests` 7/7. `load-web-modules.mjs` loads all 45 modules.
- Every `.mjs`-harness class and 104 classes in total (those plus the whole Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention family): one failure, `LivePostgresCollectionHygieneTests.EveryClassUsingTheSharedStore_IsSerializedOrDocumentsWhyNot`, which is on `dev-local-failing-tests.txt`.
- The guard-family run above covers 238 classes, including `GridToolsBehaviourTests` 9/9 (the shared renderer changed) and `WebModuleLoadTests` 7/7. The earlier full-suite run was before the review fixes; the full suite is left to CI. 0 build warnings.
- `scrubcheck` exits 0 on the diff and on this file.
- Not run: nothing required. Lite.Tests is untouched.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Dismiss Selected and Dismiss All on the web Alert History page** ([#5151]) - a seat that can edit gets row checkboxes and the desktop's two dismiss buttons, with a status line of how many alerts were dismissed.
REF: [#5151]: #5151
…tivity tab (#5150)

Part of #4843

## What users see change
- A new **Job History** entry at the end of the web nav opens a page showing SQL Agent job runs (steps and job outcomes) across the fleet, newest first, the web twin of the desktop Job History view.
- Columns follow the desktop grid: Run Time, Server, Job, Category, Step, Status, Duration, Retries, Last Success, Message. Sort, Copy and CSV come from the shared grid.
- Filters: Server (all or one), Window (1/4/12/24 hours, 7 days; default 24 hours), Job (exact name, applied on Enter or when the box loses focus), Status (all, Failed, Succeeded, Retry, Canceled), Category (the categories seen so far), and Rows (100/250/500/1000, default 100). All filters are applied by the read before its row limit. Choices, typed text and focus survive the 60 s refresh.
- The page says when the window reaches past the history the store keeps (also when the answer is empty), and when the row limit cut the list. A "N runs shown" line sits above the grid.
- Rows are coloured as the desktop grid does: failed red, long-running and retry amber, canceled grey. The desktop also dims disabled jobs; the read does not return an enabled flag on this path, so the web does not.
- Differences from the desktop tab: the windows are 1/4/12/24 hours and 7 days (the desktop also has 30 d, 90 d and 1 y; the read's maximum is 168 hours), and there is no 2,000-row cap, the Rows choice goes to 1000 at most.
- The Server choices list SQL Server targets only (PostgreSQL targets have no Agent history).
- The server page's Activity tab (SQL Server) gains a per-server Job History grid with the same columns less Server (newest 100 runs in the page range). It was not on the web before.

## What changed
- `pages/job-history.js` (new), reading `get_job_history` through `readToolWithinKeptHistory` with the render's `AbortSignal`; server choices from `list_servers`.
- `index.html` and `app.js`: one nav entry, import, route, active-nav key, each added at the end of its list.
- `server-tabs.js`: the Activity-tab grid and its column list only.
- `job-history-harness.mjs` and `JobHistoryPageTests.cs` (15 tests).
- `app.css`: three row-colour rules beside the data-table cell colours. `util.js`: `run_time` added to the instant fields, so a custom range trims the per-server grid. No C# product change.
- The job box defers its blur by a turn, so a rebuild no longer drops focus or fires a second read, and the caret is kept. The empty-answer notice reads `hints`.

## What's not changed
`panels.js`, the rest of `util.js`, `alerts.js`, the `app.js` scheduler and status bar, other `server-tabs.js` regions. The PostgreSQL Activity tab has no Job History (Agent history is SQL Server only). The desktop's Agent status indicator is not on this page (it needs a separate `get_agent_status` read).

## Verified at the head
- `JobHistoryPageTests`: 15/15 pass; all FAIL on dev's wwwroot (page and wiring removed).
- `WebModuleLoadTests` passes; module count comes from disk.
- Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention family (79 classes) plus every `.mjs`-harness class (26): 1255 run, 1 failed, `LivePostgresCollectionHygieneTests.EveryClassUsingTheSharedStore_IsSerializedOrDocumentsWhyNot`, which is in `dev-local-failing-tests.txt`.
- Full Darling.Tests (run at the previous head; this push re-ran the page, harness, load and guard classes only): 22703 run, 478 failed (377 distinct methods). All are on `dev-local-failing-tests.txt` except two `DarlingManagedPostgresTests.EnsureConfAppended_*` methods; they do not touch web files and I did not run them on a clean dev, so CI decides.
- `scrubcheck` on the diff and this file: exit 0.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Job History page in the web dashboard** ([#5150]) - A cross-server Job History page shows SQL Agent job runs with server, window, job, status, category and row-limit filters, and the Activity tab gains a per-server Job History grid.
REF: [#5150]: #5150
…igh Impact and Expensive Queries (#5156)

Part of #4843

## What users see
Rows that carry a `query_hash` get a **Plan** button in Top Queries (server page), FinOps High Impact and FinOps Optimization > Expensive Queries. It opens a panel under the row with the query's stored plan XML, indented, with **Copy** and **Download .sqlplan** (file named `<query_hash>.sqlplan`, type `application/xml`). A plan larger than the read's 500 KB cut says so and turns Download off, because a cut file will not open. A query with no stored plan shows the read's own message. The open panel stays open through the 60 s rebuild.

## What changed
- New `pages/plan-viewer.js`: `planColumn(server)` (added to each grid's column list), `storedPlanCell`, `openStoredPlan`, `prettyPrintXml`, `downloadPlan`. It reuses `copyText` from `grid-tools.js`. The XML is drawn only through `textContent` of a `<pre>`.
- `get_plan_xml` returns bare XML for the MCP clients (unchanged). The web read route wraps a stored plan as 200 JSON `{query_hash, database_name, plan_xml, truncated}`; `truncated` is decided on the server from the tool's own cut marker, which is removed from `plan_xml`. A "no plan" status or error envelope passes through unchanged. The viewer reads `plan_xml` and `truncated`; it no longer sniffs text. Params: `server`, `query_hash`, and `database_name` when the row has one.
- Expensive Queries rows now carry `query_hash` (`MAX(query_hash)` in the existing statement query, a new optional `QueryHash` on the `ExpensiveQuery` record, and a `query_hash` field in the `get_finops` optimization view's expensive-query rows), so the Plan column shows there. No tool description changed.
- Open panels are keyed by server + database + hash at module scope; a rebuilt cell redraws from that state, and a read still in flight fills the new cell too.
- Top Queries (both the tab and the standalone panel), High Impact and Expensive Queries use `[...COLUMNS, planColumn(server)]`; the column hides when no row has a hash. `css/app.css` gets three rules beside the grid-tools rules.
- Every `.mjs` harness that copies `server-tabs.js` or the finops tabs now also copies `plan-viewer.js`.

## Not changed
Get Actual Plan and a plan tree view stay out. The PostgreSQL Top Query Shapes grid has no Plan column: `get_plan_xml` is SQL Server only (pinned). No `panels.js`, `util.js`, `app.js` or `alerts.js` edits.

## Verified at the head
- Web wrapper: 3 new `DarlingWebEndpointsTests` cases (whole plan gives JSON that classifies as passthrough; a cut plan sets `truncated` using the real `McpHelpers.Truncate` marker; envelopes pass through byte-identical). `DarlingWebEndpointsTests` 76/76. The plan harness now fakes the 200 shape. `FinOpsTabOptimizationPageTests` pins the new `query_hash` row field.
- `WebExceptionTextCensusTests` 40 (1 skipped), `DarlingCustomViewsTests` 71/71 (`get_plan_xml` params unchanged; a custom view calling it now gets a `plan_xml` JSON object instead of a 400 error strip), `McpToolsListBudgetTests` 5/5, `FinOpsOptimizationViewTests` 8/8.
- RED on dev: `WebPlanViewerPageTests` 5 of 6 fail, `PlanViewerBehaviourTests` 7 of 7 fail (module absent).
- GREEN: `WebPlanViewerPageTests` 6/6, `PlanViewerBehaviourTests` 7/7 (open, text-only, download name/type/body, copy, truncation disables Download, no-plan text, state survives rebuild).
- Updated import allowlists in `FinOpsTabHighImpactPageTests` (8/8) and `FinOpsTabOptimizationPageTests` (16/16); `RepoFileAdoptionTests` 2/2.
- All 100 `.mjs`-harness and Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention classes run; only `LivePostgresCollectionHygieneTests` fails, which is on the Mac-local dev failing list.
- Build 0 errors, no new CA/CS/IDE warnings.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Stored query plans on the web dashboard** ([#5156]) - Top Queries, High Impact and Expensive Queries rows with a query hash get a Plan button that shows the stored plan XML with Copy and Download .sqlplan.
REF: [#5156]: #5156
…ger reads as a plan change (fixes #5114) (#5155)

PostgreSQL plans are now grouped by their shape. The plan hash (PgPlanLogParser.FromBlock's PlanHash) covered the whole redacted plan JSON, cost and row estimates included, so a statistics refresh, a different literal or a different work_mem gave one plan shape a new hash, and the plan-flip finding read estimate drift as a plan change. The hash now covers the shape only: it is taken over a clone of the redacted plan with the estimates, the runtime counters (Actual *, Rows Removed by *, Index Searches, Heap Fetches, sort, hash and memoize counters, Workers and Workers Launched), the buffer, I/O and WAL counters, Workers Planned, and the root's Settings, JIT, Planning, Planning Time, Execution Time and Triggers removed, recursively through every child plan. It is a denylist, so an unknown key still splits the hash. The stored plan_json is unchanged and keeps every estimate. A plan without any of those fields hashes as before.

Captured-plan lists (get_pg_plans and the viewer) fold a shape's estimate variants into one row, so captures and durations sum over the shape. Each row now shows the JSON of that shape's latest collection cycle: the reader ranks and cuts the groups first and fetches the JSON for the page only, instead of the old min(plan_json), which was arbitrary once a group held JSONs with different estimates, and aggregated kilobyte-sized text over every group.

On the first analysis pass after the upgrade, a statement captured on both sides of it may report one plan change, and captured-plan lists and the store log's slow_plan groups show pre-upgrade groups beside new ones until the older rows age out (plan retention is 14 days by default).

Tests: PgPlanShapeHashTests (new, 29 cases): estimate, settings, timing and counter variants give one hash, including on a grandchild and a SubPlan node; a different node type, join order, join method, index or parallel awareness gives another; Workers Planned is not shape; an unknown key still splits; the stored JSON keeps its estimates; the hash stays 32 hex characters. A store-log case (two plans differing only in costs are one row) and a live reader case (one group, two captures, the later capture's JSON).

Fixes #5114
)

`collection_log.target_session_id` was null on every SQL Server collector run. Monitored-server connections use Multiple Active Result Sets, and under MARS `SqlConnection.ServerProcessId` is 0 at every point: after open, with a reader open, after it closes, and on pool reuse. Measured on SQL Server 2025 with Microsoft.Data.SqlClient 7.1.1. With MARS off it is the real session id.

**The fix.** Right after a collector's target connection opens, before any reader, the runner resolves the session id:
- When `ServerProcessId` is 0, it runs `SELECT @@SPID` once, with a 5-second command timeout, and caches the answer under the connection's `ClientConnectionId`.
- That id stays the same across pool reuse, so a reused connection costs no query, and the cost is one lookup per new physical connection rather than one per run.
- The cache is bounded at 4,096 entries.
- The lookup is timed into the run's SQL time.
- A failure records null and never fails the run. A real cancellation still propagates.
- If SqlClient transparently replaced the physical connection by the time the first reader opens (a new `ClientConnectionId`), the run records null instead of a stale id.
- PostgreSQL targets still read the backend pid directly.

MARS, the pools and the application name are unchanged.

**Why not the alternatives.** Turning MARS off would break the places that run a second command while a reader is open. A per-collector application name would multiply the connection pools on every monitored server.

**Tests.**
- Unit facts for the cache: one lookup per connection id, the bound, failures not cached, a real cancellation, the key read after the lookup, and eviction of a replaced connection.
- A source pin: the lookup comes before every reader.
- Live facts against a real SQL Server: the true session id; no second lookup on a pooled reopen; a full collector run writing a non-null `target_session_id` into its own scratch store.

CI runs no SQL Server, so it skips the live facts. They were run locally.

Fixes #5132
…on Blocked Process Reports and Deadlock Graphs (#5164)

Part of #4843

## What users see
On a server page's Blocking tab:
- **Blocking grid** now shows the desktop Blocked Process Reports grid's columns, in its order and under its headers (Event Time, Blocked SQL, Blocking SQL, Source, Database, Blocked SPID, Blocking SPID, Wait Time, Wait Resource, Lock Mode, then status, isolation, transaction, priority, log used, login, host and app for each side). It has 25 columns, so the status/isolation, transaction and sessions columns sit behind the column-group toggles from #5152 (status/isolation and transaction on at first).
- **Blocked Process Reports** panel: desktop headers (Event Time, Blocked SPID, Blocking SPID, Wait Time) and a **Save XML** button per row, downloading `blocked_process_<yyyyMMdd_HHmmss>.xml`.
- **Deadlock Graphs** panel: a **Save XML** button per row, downloading `deadlock_<yyyyMMdd_HHmmss>.xdl` (the item #5139 deferred until a download helper existed).
- A deadlock graph the read cut short (`deadlock_graph_xml_truncated`) gets a disabled button with a hover reason. The tab reads with `full_graph` on, so this is the rare case. A row with no XML also gets a disabled button.

## What changed
- `grid-tools.js`: new `downloadText(fileName, parts, type)`; `downloadCsv` now calls it (same Blob, same type).
- `server-tabs.js` (Blocking/Deadlock regions only): `BLOCKING_COLUMNS` (11 -> 25) with `BLOCKING_GROUPS`; `BPR_COLUMNS` reordered with the desktop headers and an XML column; `deadlockXmlColumns` gets the XML column; `saveXmlButton` and `fileStamp` helpers; one import.
- No fetch on click: both reads already return the full XML on the row, so nothing is in flight. There is therefore no in-flight state.
- No harness `.mjs` import list changes: `grid-tools.js` is already in every harness's conditional copy list.
- Tests: `WebBlockingColumnsPageTests` (column keys against `get_blocking` / `get_blocked_process_xml` projections, desktop order and headers, group names, file names); `WebBlockingSaveXmlBehaviourTests` + `web-blocking-save-xml-harness.mjs` (Node: Blob name, type and text for both downloads, truncated disables Save, no-XML disables Save, no extra read).

## Not changed / read-widening follow-ups
- The desktop's Blocked Process Reports grid has no column the read lacks. Not on the web grid: the blocking-side transaction/priority fields, which the desktop does not show either.
- `get_blocked_process_xml` returns only 6 fields, so the Blocked Process Reports panel stays at the desktop's four data columns; the desktop's extra columns live on the Blocking grid (from `get_blocking`).
- **`.xdl`, not `.xml`:** the Viewer and Lite save deadlock graphs as `.xml`; the web uses `.xdl`, which SSMS opens as a graph (the deprecated Dashboard did the same). A follow-up can move the desktop to `.xdl` too.
- Reads not widened.

## Verified at the head
- New page tests: 6/6 pass; node behaviour tests: 4/4 pass. On dev's JS (`git checkout` of the two files, rebuilt tests kept): page 4/6 fail, behaviour 4/4 fail (RED).
- Full Darling.Tests on this Mac: Total 22912, Failed 478 (336 distinct methods); 334 are in `dev-local-failing-tests.txt`. The other two, `DarlingManagedPostgresTests.EnsureConfAppended_*` (2 methods), fail with `kernel32.dll` not loadable on macOS (a platform failure, nothing to do with this change).
- That run included `WebModuleLoadTests`, `DeadlockProcessRowsTests`, every `.mjs`-harness class and the Census/Guard/Hygiene family, which showed no failures beyond the above.
- Build: 0 warnings, 0 errors.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Web Blocking tab shows the desktop's blocking columns and saves report and deadlock XML** ([#5164]) - the Blocking grid has the desktop Blocked Process Reports columns (the wide ones behind column-group toggles), and the Blocked Process Reports and Deadlock Graphs rows have a Save XML button that downloads the XML (`.xml`) or the deadlock graph (`.xdl`).
REF: [#5164]: #5164
…art (#5162)

Part of #4843

## What users see change
The Perfmon chart on a server's page used to chart one counter chosen from a drop-down. It now matches the desktop viewer's picker and the Wait Stats picker from #5119:
- a checkbox list of the counters in the snapshot table above it, with a search box, Select All, Clear All and a Default counters button;
- the default set is the desktop's General Throughput pack (Batch Requests/sec, SQL Compilations/sec, SQL Re-Compilations/sec, Query optimizations/sec, Network IO waits), limited to what this server collects; if none of the pack is collected the first counter is checked;
- up to 10 counters at once (the 11th box is disabled with a "Limit reached" hint; the desktop's cap is 12);
- one line per checked counter on one zoomable chart. Rate counters plot their per-second figure on a per-second axis; a chart that mixes in a gauge or average counter plots plain values with no unit;
- a counter whose read fails gets a note above the chart and the other lines still draw; all reads failing shows an error plus the notes; all reads empty shows the empty state, not an error;
- checks and search text survive the 60 s refresh, per server; a checked counter that leaves the snapshot stays checked and listed; the search box keeps focus and caret.

## What changed
- `server-tabs.js`, Perfmon region only: `perfmonPanel` and `drawPerfmonTrends` (replaces `drawPerfmonTrend`; `perfmonTrendLines` is gone). One `get_perfmon_trend` read per checked counter, in parallel, with an AbortController per server for superseded draws, and the chart draws at most 10 counters whatever it is handed.
- `multi-picker.js`: three optional options (`noun`, `defaultsLabel`, `defaults`) so the wording and default set are not wait-specific. Defaults are the old values, so Wait Stats is unchanged.
- `BaselineDiscontinuityRenderCensusTests` (Lite) now looks for `export async function drawPerfmonTrends(`. Its logic still holds: that function keeps one `discontinuityNotes(trend.data)` use, and the file total stays 4. Read by hand; Lite.Tests was not run on this Mac.
- New `WebPerfmonMultiSelectBehaviourTests` (14) with `web-perfmon-multi-harness.mjs`; `WebPerfmonPerSecondBehaviourTests` expectations changed from "Per second"/"Value"/"Delta" series to one line per counter.

## What is not changed
- The snapshot table, its per-second columns, and `get_perfmon_trend`. The read takes a counter name and no instance, so the list is by counter name (the desktop also lists by counter name). The desktop's other packs are not offered.
- `panels.js`, `util.js`, `charts.js`, `app.js`, CSS (the existing picker rules are enough).

## Also
- The trend harness copies `pages/plan-viewer.js` when it exists, so it keeps loading once the plan viewer import lands in `server-tabs.js`.
- When none of the default pack is collected, the first counter is checked (the desktop checks none); this continues the page's earlier behaviour.

## Verified at the head
- `WebPerfmonMultiSelectBehaviourTests`: 15 pass (one new: a counter with no trend lists the collected counters as text, "Collected here: …", instead of pointing at a JSON path). `WebPerfmonPerSecondBehaviourTests`: 10 pass. `WebWaitStatsMultiSelectBehaviourTests`: 14 pass, unchanged.
- RED on dev: the new harness cannot pass against dev's `server-tabs.js` (no picker, no `drawPerfmonTrends`; the `defaults` scenario fails at the missing Select All / mp-count node).
- Every `.mjs` / `load-web-modules` class, `WebModuleLoadTests` and the Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention family (104 classes, 1,300 tests): 1 failure, `LivePostgresCollectionHygieneTests` (Mac-local; it cannot load WPF types here).
- `scrubcheck` on the diff: exit 0.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Web Perfmon chart charts several counters at once** ([#5162]) - the chart now has the desktop's checkbox list with search, Select All, Clear All and the General Throughput default set, up to 10 counters on one zoomable chart. Your choices are kept across the page's refresh.
REF: [#5162]: #5162
… their data starts (part of #4966) (#5165)

The web server page's PostgreSQL tabs now say where their data starts on eleven more reads: top queries, blocking, database stats, session states, I/O stats and replication stats, the database, query-duration, wait and I/O trend grids, and captured plans. WebDataStartNote lists each read with the table its rows come from; blocking, session states and replication stats are sparse (most samples store nothing) and have no retention edge, so they are probed on their own collector's runs instead (CollectorRunsByRead), and a long-monitored server whose first blocking chain or replica came recently reads as covered.

Each read's "captured, found nothing" status word is admitted where it has one (no_blocking_sampled, no_io_activity, empty for the database, session-state and trend reads), never not_sampled, unavailable or precondition; tools that answer rows with a status word and no message (session_states, blocking_sampled, cycles_only, the trend words and others) are accepted as row answers. No stat tile beside these reads shows only newest values, so no panel opts out, and server-tabs.js is unchanged.

Tests: the live PostgreSQL theories cover the 17 noted tables; a sparse read on an old server with one late row gets no note, and on a two-day-old server gets one; the status words are pinned from the tool sources and by behaviour; 11 page scenarios through the Node harness; the source pin is scoped to the listed reads.

Part of #4966
…urce, reset zoom (#5166)

Part of #4843

## What users see
Every web line chart has a small "⋯" button at its top right, and right-click on the chart opens the same menu. Items, in the Viewer's order and wording:
- **Copy Image**: draws the chart to a PNG and puts it on the clipboard (secure page only; otherwise a plain message says why).
- **Save Image As...**: downloads the PNG.
- **Reset zoom**: only while the chart is zoomed (#5092).
- **Export Data to CSV...**: one row per time and series (`DateTime (UTC),Series,Value`). It exports every loaded point, also while the chart is zoomed, the same as the Viewer. Times are UTC in three columns; the Viewer writes the display time zone plus a UTC-offset column.
- **Show Data Source**: the read name and its parameters, shown under the chart. Hidden when the chart has no read to name.

The menu is keyboard reachable: Tab to the button, Enter to open, Arrow keys to move, Escape or Tab to close. A right-click menu opens at the pointer and stays inside the chart. The open menu and the Show Data Source panel stay open across the 60 s refresh (kept at module scope per chart and page scope, for 90 s).

## What changed
- `charts.js`: `renderLineChart` attaches the menu, so every chart gets it. New optional spec fields `title`, `source` ({ read, params }), `zoomed`, `onResetZoom`. `zoomableLineChart` supplies the zoom pair. No new exports are imported by any other module.
- `panels.js`: the panel chart passes its title and its read name and params (two lines).
- `css/app.css`: menu rules beside the chart tooltip rules.
- CSV reuses `grid-tools.js` (`toCsv`, `csvFileName`, `downloadCsv`), loaded on first use. PNG: the SVG is cloned with computed styles written inline, drawn on a canvas, no external fetches.
- Copy Image hands the clipboard a promise for the PNG (Safari needs the write started inside the click); a failed CSV module load shows "Export failed".
- Tests: `WebChartMenuBehaviourTests` + `web-chart-menu-harness.mjs`.

## Not changed
- **Open in New Window** is out of scope.
- Server-tab and FinOps charts that call `zoomableLineChart` directly pass no `source`, so they show no Show Data Source until a caller passes it.
- Bar, pie and scatter charts have no menu.

## Verified at the head
- New harness on dev's `charts.js`: RED (the menu does not exist; the harness throws). On this head: 8/8 pass (adds right-click placement `right:auto` + clamped left/top, zoomed CSV = all 11 points, Tab closes, menu and source survive a rebuild).
- Every `.mjs`-harness class (incl. `WebModuleLoadTests` 7/7, `WebChartZoomBehaviourTests` 10/10, `GridToolsBehaviourTests` 9/9): 0 failures. `ChartIntegerTicksPinTests` needed the spec destructure kept as is; passes 3/3.
- This run: the targeted classes, every `.mjs` class and the guard family, 113 classes, 1364 tests, 1 failure.
- Guard family (all classes ending Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention Tests, plus `DocCommentHygieneTests`): only `LivePostgresCollectionHygieneTests` fails, and it is on `dev-local-failing-tests.txt`.
- Full Darling.Tests on this Mac: 22847 total, 478 failed, 1920 skipped. Every failing class is on the Mac-local list (parameterized rows and `ViewerChartContextMenuTests`, WPF); none touch charts.js.
- Not run in a real browser: the canvas, clipboard and menu placement are untested there (the node harness has no canvas or layout); worth one manual click-through.
- scrubcheck on this file and the diff: exit 0.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Web line charts have a menu** ([#5166]) - Each line chart has a "⋯" button (and right-click) with Copy Image, Save Image As, Export Data to CSV, Show Data Source (where the chart knows its read) and Reset zoom (while zoomed).
REF: [#5166]: #5166
…on trends (#5170)

Part of #4843

## What users see change
`get_server_trend` (and its `/api/read/get_server_trend` route) takes four more `metric` values:

| metric | a point holds |
|---|---|
| `latch` | `wait_time_ms_per_second`, one series per latch class |
| `spinlock` | `collisions_per_second`, one series per spinlock |
| `session_stats` | the server-wide session counts (`total_sessions`, `running_sessions`, `sleeping_sessions`, `background_sessions`, `dormant_sessions`, `idle_sessions_over_30min`, `sessions_waiting_for_memory`, `databases_with_connections`), averaged per bucket, plus the top application and host of the newest collection in the bucket |
| `collector_duration` | `max_duration_ms`, `avg_duration_ms`, `run_count` of successful runs, one series per collector |

- **New `names` parameter** for `latch`, `spinlock` and `collector_duration`: comma-separated, exact case, up to 10, default the top 5 (most wait time, most collisions, longest run). Names with no samples come back in `missing_names`. If none of the named ones has a sample, the answer is `empty` with a distinct note, `missing_names` and `top_names`. `names` on any other metric is refused, and `clerk_types` on these three is refused.
- Latch and spinlock rates use the viewer's rule: summed delta over the summed stored interval, and a restart's unknowable (interval 0) sample is left out rather than drawn as 0.
- Same envelope as before: house window and bucket parameters, omitted nulls, `discontinuities[]`, and `empty` / `unavailable` / `not_collected`. `session_stats` is sized as four single-field points so its default 168 h answer stays under 32 KB.

## What changed
- `ServerTrendSql.cs`: `SessionSummary` (moved verbatim from the viewer), `LatchWaits(n)`, `SpinlockCollisions(n)`, `CollectorDurations(n)`, the three top-N reads and four "ever collected" probes. The viewer's `SessionStatsSql` now points at `ServerTrendSql.SessionSummary`; I diffed the old and new text and they are identical (71 lines).
- Latch, spinlock and collector duration: the viewer's reads have a different shape (per-row rates for a fixed top 5; per-collector buckets with no name filter), so the bucketed, name-filtered reads are written new in the house style. The viewer's SQL is untouched. One new read per metric, no new Storage reader class.
- `DarlingMcpServerTrendTools.cs`: the four metrics, `names`, a shared series path for clerks, latch, spinlock and collectors, notes and `Fields` per metric. `DarlingWebEndpoints.cs`: `names` in the catalog row and dispatch. `GetServerTrend`'s internal overload takes `names` before the budget so the cancellation token stays last (CA1068).
- Budget txt, ceiling, and tests.

## What's not changed
No `wwwroot`, no `DarlingManagedRoles.cs`, no migration, no change to the four earlier metrics' output. Left for a later change: `tempdb_file_io`, `tempdb_size`, `file_io_throughput`.

## Budget
- `tools/list`: 186,010 -> 186,158 bytes (+148); `TotalCeilingBytes` set to 186,158.
- Per-file txt: tool head 279 -> 331 (text grew after the guide marker mostly), `metric` param 61 -> 44 (the list moved into the tool description), new `names` param 70.
- Largest default call at 168 h, on a week of one-minute collections (8 latch classes, 8 spinlocks, 12 collectors): latch 11,127 B, spinlock 10,831 B, session_stats 17,944 B, collector_duration 15,627 B. All under the 32 KB target. The four earlier metrics are unchanged (largest 17,555 B, cpu_scheduler). Widest explicit width: latch 106,172 B at 34 min, spinlock 103,197 B at 34 min, session_stats 149,898 B at 27 min, collector_duration 149,991 B at 34 min, all under the 320 KB cap.

## Verified at the head
- Build: Darling.Tests Release, 0 errors, no CA/CS/IDE warnings after the CA1068 fix.
- `DarlingMcpServerTrendToolsTests`: 13/13 (parameters including `names`, eight-metric list in the description, refusals, every field a note names is emitted, `missing_names` in the description, name/width binding in the SQL, viewer and tool share the session SQL text).
- `DarlingMcpServerTrendToolsLiveTests`: 2/2 on a throwaway TimescaleDB 2.30.1 / PG18 container (host port 55615, removed). The new arm covers planted values for all four metrics (100 ms/s latch, 20/s spinlock, 100 then 120 sessions with the newest collection's application, 9000 then 9100 ms slowest collector), the restart-marker sample dropped as a point, a failed run not counted, `missing_names`, the none-found empty note, `unavailable` before any collection, `empty` for a quiet window, and the 168 h sizes above. Run 4 times; one run failed once on `bucket_minutes` auto sizing (fixed by pinning 1 minute in the planted arms) and is stable since.
- `WebReadCancellationPinTests` 139/139 (its `metric=total_waits` query string still works; the new parameter is optional).
- `McpToolsListBudgetTests` 5/5, `McpToolGuideTests` 10/10, `McpToolGuideHeadsDataTests` 3/3, `BaselineDiscontinuityRenderTests` 3/3, `EngineCapabilityReadWiringTests` 6/6 (the capability question names `latch_stats`, `spinlock_stats` and `session_stats`; `collector_duration` asks no collector because the log is not a gated collector).
- Guard family: all 79 Darling.Tests classes ending in Census, Ratchet, Adoption, Discipline, Guard, Scrub, Hygiene, Pin or Convention + Tests, plus DocCommentHygiene, RepoFileAdoption, TsqlConventionGuard, FleetIdentifierScrub, QueryStoreSliceTieBreakSource, the three command-timeout classes and the viewer session/duration/latch classes: all pass except `LivePostgresCollectionHygieneTests.EveryClassUsingTheSharedStore_IsSerializedOrDocumentsWhyNot` (loads WPF types; on the Mac-local list) and `ViewerCollectorDurationChartTests.TheChart_IsFedItsOwnRead_NotTheGridsPage` (PresentationFramework; on the list).
- Full Darling.Tests once: 22,847 run, 552 failed, 247 skipped. All failing names (430 distinct after dropping theory-row arguments) are on `dev-local-failing-tests.txt`; none is new.
- RED on dev: `Metrics` has four entries, `names` and `SeriesOf`, `ServerTrendSql.SessionSummary`/`LatchWaits`/`SpinlockCollisions`/`CollectorDurations` do not exist, so the new tests fail to compile there (RED by compile failure). Before the sizing fix, session_stats was 68,530 B at 168 h, over the 32 KB target.
- Not run: Lite.Tests (never run on this Mac; the change touches no Lite file).
- Finds: 0 substantial, 1 hygiene fixed in-lane (session_stats default width sizing), 0 polish.
- Follow-up at head 96df107 (dev merged in): a latch or spinlock row with a known interval but a NULL delta no longer adds its seconds to the rate (the Viewer drops that row too); `KindMetric` is gone and `ReadTopNamesAsync` always takes the metric. New live test `TheRateRules_SessionNewestCollection_AndCollectorNames_HoldAgainstTheStore` covers the NULL-delta row (RED before the fix: 50 ms/s instead of 100), a latch row with no stored interval (previous-collection gap), two session collections in one bucket (the newest one's attribution) and `names` for collector_duration. `DarlingMcpServerTrendToolsTests` 13/13, live class 3/3 on a throwaway TimescaleDB container (removed); `McpToolsListBudgetTests` 5/5 with tools/list measured at 185,766 bytes, under the unchanged 186,158 ceiling; `McpToolGuideTests`, `WebReadCancellationPinTests` and the 80-class guard family green except `LivePostgresCollectionHygieneTests` (loads PresentationCore, Mac-only).

## CHANGELOG entry
SECTION: Added
ENTRY: - **More server trends over MCP: latch, spinlock, session stats and collector duration** ([#5170]) - `get_server_trend` now also returns the latch wait and spinlock collision rates per name, the server-wide session counts, and the per-collector run duration trend the desktop charts draw, with a `names` filter for the per-name ones.
REF: [#5170]: #5170
…l=full read for the fields the page lacked (#5169)

Part of #4843

## What users see
The server page's Top Queries grid (both the CPU tab panel and the composite panel) now has the desktop Top Queries columns in the desktop's order, 45 in all (the desktop's Query Plan download column is the separate plan work). The core set shows by default: Database, Query, Module, Execs, Total/Avg CPU, Total/Avg Duration, Total/Avg Reads, Total Spills, Query Hash. Six toggles under the "Columns:" strip turn on the rest: Times (Last Execution, Creation Time, Peak CPU ms/s, Plan Gen, Total CLR), I/O and rows, Extremes, Memory grants, Parallelism, Hashes and handles. Sort, Copy and CSV work on the wider grid; Copy and CSV carry every column. On the hourly tier the fields that tier lacks print a dash.

Query sits right of Database, not behind the two timestamps as on the desktop, because a pinned rule keeps query text next to the anchor. Module at index 2 is a choice of this grid, not a rule the pin states. Last Execution and Creation Time print in the browser's local time.

## What changed
- **Page:** `TOP_QUERY_COLUMNS` (45 keys) plus `TOP_QUERY_GROUPS`, passed to both Top Queries grids. Both ask for `detail: "full"`.
- **Read:** `get_top_queries_by_cpu` gains an optional last parameter `detail` (`summary` default, `full`). `full` adds last_execution_time and creation_time (UTC instants written with the "o" format; the read converts them from the monitored server's clock per row, as every other server-local column on the MCP surface is, and the frame census carries a row for them),  min/max physical reads, rows, grant / used grant / ideal grant KB, spills, reserved and used threads, total_clr_ms, plan_generation_num and worker_time_per_second. Fields the store holds no value for are omitted, not written as null. The raw tier only: an hourly-tier read adds nothing, and `precision_note` now says so when `detail=full` was asked and the hourly tier answered. Both aggregates (`TopQueriesSql`, `TopQueriesByHostObjectSql`) select the extra columns and the reader maps them into a `TopQueryDetail` record. The HTTP catalog and dispatch pass `detail`.
- **Size, measured live on a throwaway TimescaleDB, 168 h, top 20:** default call 12,980 bytes, `detail=full` 14,027 bytes (22 groups, only one with the detail columns, short text). A row with every detail field adds 294 bytes, so 20 full rows add about 5.9 KB; the default call is unchanged, and full stays under the 32 KB target unless query text is near its 2,000-char cap on most rows. Neither form has a byte bound: the summary call has no page cut or `truncated` flag for size (query_text is capped at 2,000 characters per row and nothing else bounds the payload), so `full` follows the same rule and adds about 6 KB at 20 rows. A size bound for both forms (a shorter text preview) is a separate follow-up that would change the summary call; it is not part of this PR. Because the default call is untouched, no opt-in was needed beyond `detail`.
- **tools/list:** new line `param get_top_queries_by_cpu.detail 122` in `McpToolsListBudget/DarlingMcpDataTools.txt`. `TotalCeilingBytes` (186,010) did not need to grow (the budget test passes).
- Tests: `DarlingCustomViewsTests` param keys and `WebServerPageRangeTests` read URL now include `detail`.

## Not changed
- `PG_TOP_QUERY_COLUMNS`: a different engine; the PG read does not return these fields.
- Lite and the desktop grid are untouched.
- The Plan column (#5156) is on dev and merged into this branch: both grids pass `[...TOP_QUERY_COLUMNS, planColumn(server)]` plus the groups.
- Peak CPU ms/s is the desktop's `WorkerTimePerSecond`: the max per-collection rate in the window.

## Verified at the head
- New `WebTopQueriesColumnsTests` 8/8: column keys against the read's projection parsed from source, desktop order, group layout (all six groups used, none on by default, core set ungrouped), both grids pass groups and `detail: "full"`, `detail` is an appended optional, both aggregates select the detail columns in the reader's order.
- RED on dev: all 8 fail (the new class run in a worktree of origin/dev).
- New live `TopQueriesDetailLiveTests` 1/1 on a throwaway TimescaleDB container (removed): reader detail values, MCP summary omits them, full carries them and omits nulls, bad `detail` refused, both sizes under 32 KB.
- Also green: `ViewerGridPayloadColumnOrderPinTests` 30/30, `McpToolsListBudgetTests` 5/5, `ServerTabColumnGroupsPinTests` 2/2, `WebServerPageRangeTests` 11/11.
- Full Darling.Tests on the Mac: 22,911 run. After fixing the three failures this PR caused (query-text order, budget txt line, range-test URL), the remaining failures are the Mac-local set (DarlingInstallLocation, Viewer WPF classes, etc.). Five live classes outside the dev-local list failed in the full run and also fail on origin/dev on my rig (TopCpuQueriesTextLive, FinOpsOptimizationTieBreak, StoreMetricsLatestSkipScanLive, ViewerFinOpsIntervalHonestLive, FinOpsOptimizationViewLive); they did not involve this change.
- The guard family and every `.mjs`-harness class were included in that full run.
- At the new head (dev merged in): `WebTopQueriesColumnsTests` 8/8, `TopQueriesDetailLiveTests` 1/1 (non-zero server offset: stored local times come back as UTC; host_object call; a 5-billion physical-reads extreme), `TopQueriesHourlyRoutingLiveTests` 7/7 (a `detail=full` call answered from the hourly tier carries the note and no detail field), `ConsumedTimestampFrameDisciplineTests` 17/17, `ViewerGridPayloadColumnOrderPinTests` 30/30, `McpPayloadContractCensusTests` 68/68, `McpToolsListBudgetTests` 5/5, `WebPlanViewerPageTests` 6/6, the 108-class guard family plus `.mjs`-harness set clean on the Mac except `LivePostgresCollectionHygieneTests` (loads WPF types; dev-local list). Full suite: 22,975 run, 568 failed, every one on the dev-local list or one of the five live classes named above.
- scrubcheck on the diff exits 0.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Web Top Queries shows the desktop grid's columns** ([#5169]) - the server page's Top Queries grid now carries the desktop's 45 columns in six toggle groups (times, I/O and rows, extremes, memory grants, parallelism, hashes and handles), with the core columns on by default. `get_top_queries_by_cpu` gains an optional `detail=full` that adds the grant, thread, extreme, time, CLR and plan-generation fields.
REF: [#5169]: #5169
…says the answer is approximate (#5094) (#5168)

get_query_store_top answers long windows from the daily summary, and says the answer is approximate (#5094)

Fixes #5094. The table path used to aggregate every interval row in its window on every call. On a large store a 168-hour read took 6-23 s cold and 2.7 s warm, almost all of it heap reads.

- When the interval table serves the read, the whole UTC days inside the window come from collect.query_store_top_daily (built by #5125), and only the partial days at the edges come from collect.query_store_interval_wide. The read takes the longest contiguous run of built days in the gate's own REPEATABLE READ, READ ONLY snapshot, with ties going to the latest run. With no built day in the window, today's statement runs unchanged (same text, same parameters).
- The summary is approximate by design (Erik's call on #5094): a write into a day after it was built is missed, a row that moves into the next day can count twice until the day's second pass, and a monitored-server clock more than an hour ahead drops rows. In the clean case the answer equals today's exactly. Answers that used the summary carry approximate: true, an approximation_note and summary_days, including the two empty answers, and the web Query Store grid shows the note. The viewer's own Query Store read stays exact.
- Read failures keep #5136's accounting: a fault after the gate chose the table, the built-days read included, is recorded as a raw fallback and answers from raw.
- Tests: QueryStoreTopDailyReadLiveTests (25 facts: equality against today's read on fixed past anchors, edge bounds, run selection, the documented double count, the empty-answer flags and the fault fallback), plus the payload-contract census and the web tab pins.
…rows that need it (inert, #5158) (#5173)

query_stats can fetch plan XML in a second target query for only the rows that need it (inert, #5158)

Part of #5158, the first of three PRs. Nothing sets the new switch yet, so no collector's SQL or behavior changes.

- With plan capture on, every query_stats run renders sys.dm_exec_text_query_plan for all of its ~200 rows and ships the XML, although almost all of those plans are already stored. #5158 measured 53.75 GB/h rendered and 28.83 GB/h shipped on one large store, against 1.14 GB/h of distinct plans.
- CollectorContext.DeferPlanXmlFetch (default false) leaves the plan fragments out of BuildQuery and skips the plan ordinals in ReadAsync, through one shared predicate.
- QueryStatsCollector.BuildPlanFetchQuery builds one statement for a list of (plan_handle, start offset, end offset) keys: an inline VALUES list of host-rendered 0x literals, OUTER APPLY sys.dm_exec_text_query_plan, the same size-cap CASE and byte column as the inline fragment, and OPTION(RECOMPILE). It throws when capture is off, the list is empty, or a handle is not 1 to 64 bytes. ReadPlanFetchAsync maps each ordinal to its plan and size.
- Row.KnownPlanDigest and ICollectorRowWriter.PayloadOrDigest (default: Value(content)) let a later writer store a known digest in place of the plan text. WritePayload calls it, and with the default every row writes exactly as before.
- Tests: QueryStatsPlanFetchTests (18) and QueryStatsSqlGoldenTests (4 SHA-256 pins of the full BuildQuery text for Standard and Azure SQL DB, capture on and off, computed on dev and unchanged here).
…n cache trends (#5161)

Part of #4843

**Stacked on #5162 (Perfmon multi-select): this branch contains that PR's commits and uses its new `multi-picker.js` options, so it must land after #5162.**

## What users see
- **CPU tab:** a "CPU Scheduler Trend" chart (runnable, blocked and queued task counts; axis "tasks"), as the desktop's CPU Scheduler chart draws them.
- **Memory tab:** a "Memory Clerks Trend" chart (MB) with a clerk selector, and a "Plan Cache Trend" chart (single-use and multi-use MB).
- The clerk selector is the shared multi-select (#5119), capped at the read's 10 clerk types. Its options are the clerks of the latest snapshot, heaviest first; the heaviest five are checked the first time, as on the desktop. The checked set and search text survive the 60 s rebuild and are kept per server.
- Clerk types the read found no samples for are named in a notice. Baseline discontinuities show through the existing `discontinuityNotes` helper. An empty answer shows the read's own message.
- All three charts use the zoomable chart path (#5092).

## What changed
- `server-tabs.js`, CPU and Memory tab regions only: `serverTrendPanel` (cpu_scheduler, plan_cache), `memoryClerksTrendPanel` and `drawClerkTrends`, with no new import beyond `multi-picker.js`. The clerk picker passes `noun: "clerk"`, `defaultsLabel: "Top clerks"` and `defaults` = the heaviest five, so its button, hint and empty-search text say clerk, and the button checks five like the desktop's Top Clerks. When no named clerk has samples, the missing list and the heaviest clerks come from the empty answer's `hints` and show as text.
- Reads go through `readToolWithinKeptHistory` with `{ server, metric, hours }` (and `clerk_types` for clerks), so the page range, the custom range's `as_of` (#5108) and the kept-history narrowing apply like the sibling panels.
- `Lite.Tests/BaselineDiscontinuityRenderCensusTests`: the census counted four `discontinuityNotes(...)` uses; it now counts six and names the two new functions (`serverTrendPanel`, `drawClerkTrends`). After merging #5162 the census counts six and lists `drawWaitTrends`, `drawPerfmonTrends`, `drawQueryTrend`, `fileIoPanel`, `serverTrendPanel`, `drawClerkTrends`; I checked each boundary by hand (one use per function, 6 in total). Lite.Tests builds here but cannot run; I traced the regex `discontinuityNotes\((trend\.data|res\.data)\)` by hand: the new uses are `discontinuityNotes(res.data)` and `discontinuityNotes(trend.data)`, so 4 + 2 = 6.
- Tests: `WebServerTrendsPageTests` (read names, params, placement), `WebServerTrendsBehaviourTests` with `web-server-trends-harness.mjs` (series and unit, selector defaults, state across a rebuild and per server, `missing_clerk_types`, empty answers, no trend read when no clerk snapshot).

## Not changed
The panel context carries no abort signal, so these reads are not aborted by the page's 60 s rebuild (they mount into a detached node, as the Wait Stats and Perfmon panels do); a newer clerk draw does abort the older one. The clerk options come from the latest snapshot, not every clerk seen in the window.

No other `server-tabs.js` region, `panels.js`, `util.js`, `charts.js`, `app.js` or CSS. No tool or store change.

## Verified at the head
- RED on dev: `WebServerTrendsPageTests` 5/5 fail, `WebServerTrendsBehaviourTests` 7/7 fail (dev's `server-tabs.js` with the new tests).
- GREEN: `WebServerTrendsPageTests` 5/5, `WebServerTrendsBehaviourTests` 9/9 (new: picker wording and the five-clerk defaults button; hints lists on the all-missing answer), `WebModuleLoadTests` 7/7, Perfmon and Wait Stats classes unchanged (15, 10, 14).
- Every `.mjs`-harness class and the guard family (104 classes, 1,300 tests): only `LivePostgresCollectionHygieneTests` fails, which is on the Mac-local list.
- Lite.Tests: build only; not run.
- scrubcheck on the diff exits 0.

## CHANGELOG entry
SECTION: Added
ENTRY: - **CPU scheduler, memory clerk and plan cache trend charts on the web server page** ([#5161]) - The CPU tab charts runnable, blocked and queued tasks; the Memory tab charts memory clerks (with a clerk selector) and plan cache size, matching the desktop.
REF: [#5161]: #5161
… and CSV (#5183)

Part of #4843

## What users see change
- Every data-grid column header has a small ▾ button. It opens a text box: rows whose rendered text in that column contains the text (any case) stay, the rest hide. A list-valued cell matches on the text it shows.
- Filters on several columns combine with AND.
- A filtered column's button is highlighted. A strip above the table lists each active filter with a × to clear it, a "Clear all filters" button, and "Showing N of M rows".
- Escape closes the box and returns focus to the header button. Focus leaving the box closes it.
- Copy all and Export CSV carry the filtered rows, in the current sort order, with every column (hidden and grouped-away ones too), as the desktop export does.

## What changed
- `panels.js`: `gridFilter` (called from `vizTable`), module-level filter state, and `gridTools` now reads the rows not hidden by a filter. `reapplyGridSort` also re-applies the filter for grids reconciled in place.
- `css/app.css`: the filter rules, beside the existing grid-tools and column-picker rules.
- State keyed like the sort state (`gridSortKey`: route + table identity + column set): filter text per column, and which box is open (plus whether it had focus). The 60 s repaint keeps both; another server's route starts unfiltered.
- Opt-outs: `filter: false` on the descriptor or a column; `copy: false` control columns get no button.

## Not changed
Column groups, `copy: false`, sorting, drill state, `grid-tools.js` helpers (reused as they are). Copy cell / Copy row act on the picked cell wherever it is.

## Verified at the head
- New `GridFilterBehaviourTests` (node harness `web-grid-filter-harness.mjs`): 8/8. RED on dev: 7 of 8 throw (no filter button exists) and the opt-out test fails on its per-column expectation.
- `GridToolsBehaviourTests` 9/9, `ColumnGroupsBehaviourTests` 6/6, `GridSortBehaviourTests` 10/10, `WebModuleLoadTests` 7/7, `ServerPageTabsTests` 19/19; all `.mjs`-harness classes green.
- Guard family (93 classes): all green. `PlanForceActionDetailCensus` and `SqlTextPin` are not test classes (0 tests).
- Classes naming changed files/symbols (FinOpsTab*PageTests, Web*Tests, DarlingWebAssetsTests and others): green. `RawWindowFloorViewerPortTests` fails 3 on this Mac; it is a Viewer/WPF class and unrelated to this diff.
- `node --check` on `panels.js`: clean. Not run: `LivePostgresCollectionHygieneTests` (WPF load on this Mac), Lite.Tests (no Lite file names a changed symbol).

## CHANGELOG entry
SECTION: Added
ENTRY: - **Per-column filters on the web grids** ([#5183]) - each column header opens a filter box that matches the text you see; filters combine, show "Showing N of M rows", survive the 60 s refresh, and Copy all and Export CSV carry only the filtered rows.
REF: [#5183]: #5183
…ory pressure tools say where their data starts (part of #4966) (#5181)

Six Darling MCP tools now say where their data starts, with the contract Lite's twins and Darling's other window-floor tools use: get_server_config_changes, get_database_config_changes, get_trace_flag_changes, get_collection_log (one server), get_plan_corrections and get_memory_pressure_events write effective_start (UTC, with a Z), window_truncated and truncation_note right after hours_back on a data answer, and the same keys under hints on an empty answer. not_collected and unavailable answers are unchanged, the fleet form of get_collection_log carries no notice, and a failed coverage probe costs the notice, never the rows.

Coverage sources: the config tools probe their own snapshot tables (server_config, database_config, trace_flags), with the viewer's and the web's rule that a snapshot from before the window is not coverage and a logged run in the window is; the config collectors run at every connect and every 24 hours, and every run is logged. get_collection_log probes the collection log and ignores the collector, status and duration filters. get_plan_corrections probes plan_correction; an answer of only automatic-tuning rows is a data answer, and its sentence no longer calls it empty. get_memory_pressure_events probes the events on sample_time, edged at the purge cutoff. The config tools and memory pressure decide not_collected before they probe.

The web page: WebDataStartNote.AddAsync now strips a listed read's own three keys on every answer it returns, not only when it adds its note, so a covered or short window no longer hands the page the tool's UTC verdict. The memory pressure page read is not on the web's list; its empty grid shows the tool's own note when the window was cut.

Tests: the config tools' live class (a notice when collection starts inside the window, a quiet start, an empty answer with hints, the key order, a short window, the pre-window snapshot with and without a run, a failed probe); the collection log, plan correction and memory pressure live classes (the same cases, plus capped pages, a tuning-only answer that never calls itself empty, and an event sampled before the first collection); a not_collected answer that starts no probe; the web live tests assert the tool's keys are absent; census and EffectiveStartUtcTests rosters. Response budgets at the default call stay under 32,768 bytes.

Part of #4966
…ocked process report collector when the chart draws its reports (part of #5098) (#5159)

In Lite and the desktop viewer, the Blocking trend, the blocking duration Stats pair and the Overview blocking chart draw blocked process reports from the Extended Events session whenever the window holds any, and the DMV blocking snapshots only otherwise. Their "Showing since" note probed both collectors' coverage, so when the DMV collector covered the whole window but the blocked process report collector only started collecting midway, the note said covered while the chart drew zeros before that collector's first run. That stretch now gets the note.

Each app gains a cheap existence check with the read's own predicate (server, event-time window, database filter, and the viewer's collection-time floor). When the read took the reports, the note's coverage is that collector's alone. In Lite that is GetQueryWindowFloorAsync with includeAlsoCovered: false. In the desktop viewer it is the collector's own runs in the collection log: a run before the window means covered, otherwise the earlier of its first run in the window and the earliest report in the window (a first collection can store earlier reports); the shared table and run probes both start at the server's registration and so could not see a collector turned on later. When the window starts before the collection log's retention horizon, the table rule answers, as the Blocked Process Reports grid's does. The DMV branch, the deadlock notes and the grid are unchanged.

Not covered: a server whose collector ran throughout but whose blocked process threshold was set midway (the collector logs successful runs with no rows, which its runs cannot tell from quiet hours), and a collector that stopped and started again inside the window; #5098 stays open for the first.

Tests: the desktop viewer's coverage class gains the midway case (it failed on the earlier code), a report older than the first run, runs before the window, a window before the log's horizon, no reports, and reports covering the window; Lite's blocking-chart and Overview classes gain the same cases plus a database-filter pin; the call-site pins in both apps follow the new calls.

Part of #5098
…5097) (#5163)

Slow reads can now be read back with what they were asked for and where the time went. The MCP tools, the web read routes and the Custom Views panels each record a read that ran 5 seconds or more, or ended in a timeout, an error or a limit. The new `collect.slow_reads` table (migration V162) stores:
- the surface, the route and the outcome;
- the total time and, for reads that count them, the rows returned;
- the server and the time window asked for;
- the source chosen (raw, the interval table, hourly edges) and the gate's reason;
- each statement's time, up to 50 statements.

**Statement times** come from one process-wide Npgsql activity listener. It samples only inside a recorded read, so collector and background commands create no activity. A statement's time runs until its reader closes, so it includes row streaming. Only the product's SQL text feeds a statement's label, never the connection details.

**Arguments** keep only short scalar values:
- numbers and booleans;
- strings of 128 characters or fewer that look like neither JSON nor a credential.

Anything else is stored as `[omitted]`:
- JSON payload strings such as `add_servers`' `servers_json`;
- URL user-and-password parts;
- `Password=` or token text.

Keys with secret-like names are redacted at any depth. Web query keys are bounded, and unknown Custom Views sources are recorded as `compose:unknown`.

**Storage.** Recording is off the request path. A bounded queue feeds one background writer, which keeps 30 days and at most 10,000 rows, purging at most once a minute.

**Reading it back.** `get_slow_reads` returns, within the shared 32 KB response budget:
- a per-route summary, then the reads newest first;
- times in UTC;
- the five slowest statements by default, or all of them with `full_detail`.

The web dashboard reads the same answer through `/api/read/get_slow_reads`. The tools/list budget moves to 187,103 bytes and 172 tools, a figure that also covers #5186's new `get_top_procedures_by_cpu.detail` parameter, which lands with it.

**Tests.**
- The rung, the recording rule, the queue and the purge throttle.
- The argument scrub: a planted password in `servers_json`, nested API keys, URL credentials, BOM-prefixed and malformed JSON, and web tokens.
- An acceptance test that slows a `get_query_store_top` call and reads it back with `get_slow_reads`, with its `hours_back`, `top`, source, row count and statement times.

Part of #5097
…l=full read for the fields the page lacked (#5186)

Part of #4843

## What users see
The server page's Top Procedures grid (CPU tab and the composite panel) now has the desktop grid's columns in the desktop's order, 28 in all. The core set shows by default: Database, Procedure, Type, Execs, Total/Avg CPU, Total/Avg Duration, Total/Avg Reads, Total Spills. Three toggles under "Columns:" turn on the rest: Times (Last Execution, Cached Time), I/O and spills (Total Writes, Physical Reads, Avg Spills) and Extremes (min/max CPU, duration, reads, physical reads, writes, spills). Copy and CSV carry every column. Last Execution and Cached Time print in the browser's local time. On the hourly tier the fields that tier lacks print a dash.

## What changed
- **Page:** `TOP_PROC_COLUMNS` (28 keys) plus `TOP_PROC_GROUPS`; both grids ask for `detail: "full"`.
- **Read:** `get_top_procedures_by_cpu` gains an optional last parameter `detail` (`summary` default, `full`). `full` adds last_execution_time and cached_time (UTC instants written with "o", converted per row from the monitored server's clock through `DarlingServerClockReader`), avg_spills, and min/max logical reads, physical reads, logical writes and spills. Fields the store holds no value for are omitted. Raw tier only; an hourly answer to `detail=full` says so in `precision_note`. `TopProceduresSql` selects the ten extra columns; the reader maps them into a `TopProcedureDetail` record. HTTP catalog and dispatch pass `detail`.
- **tools/list:** one new line, `param get_top_procedures_by_cpu.detail 122`. `McpToolsListBudgetTests` 5/5; `TotalCeilingBytes` unchanged (the tool description itself is untouched).
- **Frame census:** inventory row for `cached_time`, `last_execution_time` ambiguous-site count 2 to 3, plus a pin that the reader converts and the tool writes "o".
- Sibling pin `WebTopQueriesColumnsTests` scoped to the queries read (it counted every `detail: "full"`).

## Not changed
- Desktop Query Plan column (separate plan work); the desktop's Plan Handle column is also left out (the read already returns plan_handle as a field, but the page has no plan-handle column here; the Plan column work covers it).
- Hourly tier gains nothing; no new tool; Lite and desktop untouched.

## Verified at the head
- New `WebTopProceduresColumnsTests` 7/7; RED on dev: 7/7 fail.
- New live `TopProceduresDetailLiveTests` 1/1 on a throwaway TimescaleDB (removed): non-zero server offset converts both timestamps, 5-billion physical-reads extreme, nulls omitted, summary lean, bad `detail` refused.
- `TopProceduresHourlyRoutingLiveTests` 2/2 (extended: `detail=full` on the hourly tier carries the note and no detail field).
- `ConsumedTimestampFrameDisciplineTests` 17/17, `WebTopQueriesColumnsTests`, `McpToolsListBudgetTests` 5/5, `McpPayloadContractCensusTests` 68/68.
- 293 guard-family and symbol-named classes: 4,314 run, 12 failed; 11 are on the Mac-local list (WPF/Viewer classes), the 12th was the queries pin fixed above (now green).
- Lite classes naming the read (VM run): CpuAttributionTests, LiteMcpServerClockTests, McpReadToolBudgetTests, McpToolGuideHeads.* , McpToolsListBudgetTests, QueryStatExtremesTests, QueryStatsCpuRankingReaderTests, QueryWindowTruncationTests. None name a symbol this diff changed.
- `node --check` on server-tabs.js clean.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Web Top Procedures shows the desktop grid's columns** ([#5186]) - the server page's Top Procedures grid now carries the desktop's columns in three toggle groups (times, I/O and spills, extremes), with the core columns on by default. `get_top_procedures_by_cpu` gains an optional `detail=full` that adds the times, average spills and read/write/spill extremes.
REF: [#5186]: #5186
…r data starts (part of #4966) (#5171)

Darling's get_blocking, get_deadlocks, get_deadlock_detail, get_blocked_process_xml and get_long_query_completions now say where their data starts, with the contract Lite's twins (#5127) and Darling's session tools (#5147) use: effective_start (UTC, with a Z), window_truncated and truncation_note right after hours_back on a data answer, the same keys under hints on an empty answer, not_collected and unavailable unchanged, and a coverage probe that fails costing the notice, never the rows.

These are event lists and a first collection can store events from before itself, so the floor on a data answer is the earlier of the coverage probe and the oldest event shown (DarlingMcpWindowNotice.ReadEventAsync). Coverage is DataWindowFloor's table rule, as in Darling's other coverage notes: the deadlock tools probe deadlocks, get_blocked_process_xml the blocked process reports alone (the XML exists only there), get_blocking both the reports and the DMV blocking snapshots with the earlier winning because its page merges both, and get_long_query_completions its own table. A collector turned on after the server was registered reads as covered from the registration. The web server page's Blocking, Deadlocks, Deadlock Graphs, Blocked Process Reports and Long Query Completions grids now show the tool's own note when its window was cut, as the Top Queries grids already do.

Tests: for each tool, a notice when collection starts inside the window, none on a quiet start, an event older than its collection names the event, hints on an empty answer, the key order, a short window that starts no probe, a capped page, and a failed probe that keeps the rows; the response budgets at the default call all stay inside 32,768 bytes; the census rosters and the shared-formatter pin cover both files.

Part of #4966
…module map plus a small recent overlay (#4605) (#5179)

Custom Views: the recent-window route resolves module names from the module map plus a small recent overlay (#4605)

Part of #4605. A fleet-wide 24-hour Custom Views panel grouped by database and object still failed on a large store: on the hourly-rollup-with-raw-edges route, its module-name lookup ranked every procedure_stats row in the window (about 8.5M rows, roughly 1.6 GB of sort) and hit the 1 GB temp_file_limit.

- collect.module_map now carries a watermark in a new runtime table, collect.module_map_state (set up idempotently like module_map, no migration). The watermark is the newest procedure_stats collection_time the last refresh read, written in the same statement as the upsert. It only moves forward, and it is never written later than the store's own clock.
- A new hourly incremental refresh, the sixth hourly store-maintenance tenant, upserts the map from procedure_stats since the watermark less 10 minutes, at most 2 days back. It is failure-isolated. A refresh capped by that 2-day bound logs a Warning naming the span it did not re-read. The daily refresh advances the watermark the same way.
- On that route, for a plan with the module join, the panel reads the watermark inside its own REPEATABLE READ snapshot, under a savepoint. The lookup then ranks only the rows from max(window start, watermark less 10 minutes) to the window end, and takes every older handle from the map (last_seen within the window, same server scope, not already in the overlay). Every other route, and a missing or future watermark, compiles exactly as before. MCP's run_custom_view_panel runs through the same path.
- The answer matches the raw route's. The documented edges: a module renamed in the minute between the window end and now, or a handle seen only in that minute; a procedure_stats row committed more than 10 minutes after its collection_time, which waits for the next daily refresh; and a refresh stalled for more than 2 days.
- Tests: DarlingModuleMapRecentRefreshLiveTests (11), new compiler facts in DarlingComposeTests and ComposeParameterCoverageTests (bind sites 23 to 24), and ComposeHourlyRawEdgesModuleMapLiveTests (8), which compares the overlay route against the raw compile row for row across seven cases.
…erage clamp (#5190)

`PayloadDimensionLiveTests.DimensionGc_PrunesOrphansButKeepsReferencedContent_WhileTheCoverageClampHoldsFacts` depended on test order. It needs the store's coverage clamp to hold the query_stats facts. But sibling tests in the class bring continuous aggregates up through `SatisfyRollupCoverageAsync`, and leave them standing in the shared store. Whenever one was still standing, query_stats read as covered, the clamp did not hold, and the test failed with "the clamp must be holding query_stats for this test to mean anything". Whether it passed depended on which siblings ran first.

The test now drops any standing continuous aggregates at the start of its body. Its `finally` recreates exactly the set that existed before, through `LiveStoreCleanup.RunAsync`. No assertion changed.

**Tests.**
- With a sibling's aggregates seeded first, the old body fails with that message and the new one passes.
- With 27 aggregates already standing, it passes.
- Alone, three runs in a row, it passes.
- The whole class passes, 18 of 18, with no aggregate left behind.

Test only.
…y Completions and Plan Corrections (#5195)

Part of #4843

## What users see
The three grids #5099 widened now follow the wide-grid rules #5169 set for Top Queries. Each starts on its core columns, with a "Columns:" strip of toggles that are all off at first:
- **Query Store Regressions:** "CPU and reads" (baseline and recent CPU, baseline and recent reads) and "Executions and plans" (baseline and recent executions, baseline and recent plan counts).
- **Long Query Completions:** "I/O and rows" (logical and physical reads, writes, rows) and "Session" (SPID, app, login).
- **Plan Corrections:** "Plans" (regressed and last-good plan ids, forcing type, forced, force failure), "Plan metrics" (the four execution-count and CPU-average columns) and "Lifecycle" (Valid Since, Last Refresh, Executed By/At, Reverted By/At).

The choice survives the 60-second rebuild, per grid. Copy and CSV still export the filtered rows in the current sort order and carry hidden columns (#5183 is untouched).

## What changed
- `server-tabs.js`: `QUERY_STORE_REGRESSION_GROUPS`, `LONG_QUERY_GROUPS`, `PLAN_CORRECTION_GROUPS` (all `defaultGroups: []`), a `group:` on the wide columns, and the groups passed to the three grids. Only those three grid regions changed.
- Tests: `WebQueryStoreGridGroupsTests` plus `web-qs-grids-harness.mjs` (the shipped page run under Node with TZ pinned to America/New_York): default heads and toggles, toggling, survival of a rebuild, local-time rendering of every timestamp column, and a source pin that every group named by a column is declared and none is on by default.

## Timestamps
No read changes. All timestamps these grids show already arrive as UTC instants written with `"o"` and use `format: "time"`; the census already registers each as Utc: `query_store_stats.last_execution_time`, `long_query_completions.event_time`, and the four `plan_correction` lifecycle stamps (measured UTC against `collection_time`). So no census row was added and none is frame-ambiguous. The new test shows a UTC instant rendering as 12:30 PM under a UTC-5 browser.

## Not changed
- No read was widened. Plan Corrections' Script, Executable and Revertable columns stay absent: they come from stored columns `get_plan_corrections` does not return (separate work).
- `panels.js` and the other tab regions of `server-tabs.js` are untouched.

## Verified at the head
- New class `WebQueryStoreGridGroupsTests`: 8 of 8 pass. Against dev's `server-tabs.js`: 7 of 8 fail (RED); the eighth is the timestamp source pin, which holds on dev already.
- Guard family (75 classes), every `.mjs`-harness class (37), the named classes (`ServerTabGridColumnPinTests`, `WebTopQueriesColumnsTests`, `ConsumedTimestampFrameDisciplineTests`, `ViewerGridPayloadColumnOrderPinTests`, `WebModuleLoadTests`, `ServerPageTabsTests`, `GridFilterBehaviourTests`, `ColumnGroupsBehaviourTests`), `DocCommentHygieneTests`, `RepoFileAdoptionTests`, `TsqlConventionGuardTests`: 1388 run, 0 failed, 24 skipped. `LivePostgresCollectionHygieneTests` excluded (WPF load on Mac).
- Changed-symbol sweep: 65 Darling classes name server-tabs.js or the grid constants: 871 run, 13 failed, all in `ViewerDataStartBannerTests`, `RawWindowFloorViewerPortTests`, `ViewerQueryStoreRegressionsDataStartTests`, `ViewerCollectorDurationChartTests` (WPF/COM `PlatformNotSupportedException` on this Mac; none touch the web grids).
- Lite.Tests naming a changed file: `BaselineDiscontinuityRenderCensusTests` (names `wwwroot/js`) needs a VM run.
- `node --check` on server-tabs.js: clean. Release build 0 errors.

## CHANGELOG entry
SECTION: Changed
ENTRY: - **Web Query Store Regressions, Long Query Completions and Plan Corrections grids put their wide columns behind column toggles** ([#5195]) - The extra columns are off until you turn on their group, and the choice survives the page refresh.
REF: [#5195]: #5195
…llapsible sidebar, kept per browser (#5196)

Part of #4843

## What users see change
- A star on every server (sidebar and fleet cards). Favourites sort ahead of everything else in the sidebar and the fleet grid, including inside tag groups and under every sort choice.
- An alert-count badge on each server (sidebar and fleet card), counting the last 24 hours of actionable alerts. Muted, resolution and dismissed rows do not count. Its colour is the critical or warning colour. Clicking it acknowledges: the badge hides in this browser until a newer alert arrives for that server (ack-until-worse, the desktop viewer's rule). Alert delivery is untouched.
- An "Alert severity colors" disclosure at the foot of the sidebar: one colour per severity (critical, warning, info) and a reset. The choices drive the badge colour and the alert history severity cells.
- A collapse button in the sidebar header; the collapsed state is remembered.

## Decision to overturn if wrong
Favourites and acknowledgements live in the browser's `localStorage`, not the store (the desktop keeps both in per-machine files too). Cost: they do not follow a user to another browser or machine, exactly as the desktop's do not follow them to another PC. This was decided while the owner was away; one line to reverse it. No table, migration, grant or route was added.

## What changed
- New `js/viewer-local.js` (no imports, no DOM, module-scope state, `localStorage` as the durable copy) and `js/viewer-local-ui.js` (controls, built with `el`/`mount`, text only).
- `app.js`: sidebar sort and badges, one block wiring the controls at start. `pages/fleet.js`: favourites-first wrapper on the existing comparators, star and badge on the card. `index.html`: collapse button and a settings host. `css/app.css`: rules beside the sidebar and `.status-cell` rules; `theme.css` and `panels.js` untouched.
- The badge reads `get_alert_history` once per 30 s, shared by every caller, so a poll tick costs the store one extra read (the same read the Alert History page makes); a failed read keeps the last counts.

## Storage keys (all versioned `v: 1`)
`darling.local.favorites.v1` (server ids), `darling.local.acks.v1` (server id to ack instant), `darling.local.severityColors.v1` (severity to `#rrggbb`), `darling.local.sidebar.v1` (collapsed flag). Only ids, instants, booleans and hex colours are stored; no names, strings from the server or secrets.

## Corrupt data
Absent, unparsable, non-object, other-version or wrong-typed values read as defaults without throwing. Inside a right-version value, invalid entries (non-positive or non-integer ids, bad instants) are dropped one by one. A colour is accepted only as `#rrggbb` (on read and on write), reaches CSS only as a `--sev-*` custom property via `style.setProperty`, and never enters markup; anything else falls back to the palette. Unreadable or unwritable storage (private mode, quota) leaves the page working for the session.

## Not changed
No server code, schema, route or grant. `theme.css` (tokens only) and `panels.js` are untouched.

## Verified at the head
- `ViewerLocalStateBehaviourTests`: 7 pass (node-run, stubbed storage): favourites order and rebuild, ack until newer alert and rebuild, corrupt/foreign/unreadable storage, non-hex colour fallback and hand-edited value, sidebar state, one shared read, wiring pins. RED on dev: the module does not exist there, so every scenario fails to load.
- Existing node harnesses that load `app.js` / `pages/fleet.js` (`web-scheduler`, `web-kept-history`) got stand-ins for the new imports; `WebModuleLoadTests`, `WebRenderSettleTests`, `WebPageRefreshIntervalBehaviourTests`, `WebServerPageRangeTests`, `WebRangeKeptHistoryBehaviourTests`: 51 pass.
- 127 guard-family, `.mjs`-harness and Web classes: 37 failed on the first run, all from the missing stand-ins above; fixed and re-run green. A further 23 pages-and-shell classes (`ServerPageTabsTests`, `GridFilterBehaviourTests`, `AlertNotebookRenderBehaviourTests`, `FleetPageAttentionFilterTests`, `StatusBarBehaviourTests`, and others): 387 total, 0 failed.
- Full Darling.Tests on this Mac: 23154 total, 479 failed. Every failing class is on the known Mac-local list (WPF load, PowerShell deploy tests); none names a file in this diff. CI decides them.
- `node --check` clean on `viewer-local.js`, `viewer-local-ui.js`, `app.js`, `fleet.js`; load harness clean. Release build `--no-incremental`: 0 warnings, 0 errors.
- Changed-symbol pins: grepped both test projects for `app.js`, `fleet.js`, `index.html`, `app.css`, `refreshSidebar`, `redrawCards`, `SORTS`, `server-item`, `sidebar`, and both edited harnesses. Darling classes that named one were run (above). Lite.Tests hits: `LiteSidebarDotRendersTheCardStatusTests`, `LiteFleetViewTests`, `LiteOverviewCardExplainsItselfTests` match on desktop/Lite source, not on these web files; list for a VM run to be safe.

## CHANGELOG entry
SECTION: Added
ENTRY: - **Web dashboard: favourites, acknowledgeable alert badges, severity colours and a collapsible sidebar** ([#5196]) - servers can be starred and sort first, each server shows an alert-count badge that a click acknowledges until a newer alert arrives, the three severity colours are choosable, and the sidebar can be collapsed. All of it is kept per browser in local storage.
REF: [#5196]: #5196
erikdarlingdata and others added 29 commits October 8, 2026 16:09
…d in the background (#5581) (#5585)

After a big retention drain, warm each server's Query Store floor read in the background (#5581)

A big retention purge leaves many dead index entries in the Query Store interval tables. The first floor read for each server then walks every one of them. On a large store that read took 19.6 s for one server. The retention pass now runs the same floor read once per server when it finishes, one server at a time. A user's first read then finds the entries already marked.

It runs only when the purge deleted rows or failed. A pass that only drops whole day partitions skips it, because a dropped partition takes its index entries with it.

The warm-up runs in the background purge task, never on the sweep loop. A server whose table is locked is skipped and logged, and a failure never fails the purge. On a copy of that store, the warm-up took 645 s for 43 servers, and the reads after it took 16 ms in total.

Fixes #5581
…hed (#5586) (#5590)

Line, scatter and ranked bar charts in the web viewer now draw at the width of their panel. Axis text and bar labels keep their normal size on a wide screen and on a phone. Before, a line or scatter chart drew a fixed 1000 by 320 picture that the page stretched sideways but not vertically. Its text was about 2.1 times too wide at 2,000 px and shrank on narrow panels. The bar chart drew a 1000-wide picture that scaled with the panel, so its labels grew and shrank with it.

A resize redraws the charts once per frame and fetches nothing. The zoom, hidden series and event markers survive it. The x axis time labels follow the width, so none overlap on a phone and none repeat. Whole-minute labels serve a zoomed window of a few minutes, and seconds a shorter one. A window across a clock change labels each tick with its time zone. The bar chart cuts a long label to fit, and a threshold line stops at a value.

A chart measured before is drawn at that width on the next refresh, so it is built once. The scatter chart's last x label and its unit caption stay clear of each other. Printing a page scales each chart to the page width. Charts draw down to a 200 px box, so a phone shows them whole.

Fixes #5586
…, #5594) (#5593)

Query Store interval tables stop cancelling their own autovacuum (#5589, #5594)

Two things the service did on the Query Store interval tables cancelled a running autovacuum, so autovacuum on those tables never finished.

The interval arm (#5589). While a table is not promoted, its legacy table is the whole live table. A monitored clock can put its newest first_execution_time more than one retention horizon ahead. In that state two loops took an ACCESS EXCLUSIVE lock every hour before they read that time. Each request cancelled the autovacuum.

The arm now reads the newest time first, without a lock. When that time is too far ahead, it warns once and returns without a lock request.

BRIN autosummarize (#5594). The collection_time BRIN index had autosummarize on. Each new block range queued a summarize request that waits for the table's lock, and after one second that wait cancelled the autovacuum. After a large purge this happened about every 2 minutes.

Autosummarize is now off on every BRIN index of both interval tables. New indexes and new day partitions get it off. The hourly maintenance pass turns it off on existing indexes, which cancels a running autovacuum at most once per index. It skips an index while a vacuum that cannot be cancelled runs on its table, such as a manual VACUUM or an anti-wraparound autovacuum. Otherwise it waits for the lock for at most deadlock_timeout plus one second, and never more than 5 seconds.

The same pass now summarizes new ranges itself. It waits for a lock for at most half of deadlock_timeout, so it gives up before the wait cancels an autovacuum. It skips a table that is being vacuumed. It works one range at a time and stops between ranges when its time runs out, so it leaves no range half done.

On a test server with steady inserts and a slow vacuum, autovacuum was cancelled 69 times in 4 minutes with autosummarize on, and never finished. With it off, nothing cancelled it and the vacuum finished.

Fixes #5589
Fixes #5594
…failed green Darling runs (#5596) (#5598)

The Darling whole-tree guards job (and some Lite test shards) failed or lingered after every test had passed. The runner printed "Waiting 10 seconds for foreground threads to exit..." and then "[FATAL ERROR] Foreground threads were left running, forcing process exit", with exit code 1 and 0 failed tests. No thread was left running. The runner only reports that the process was still alive 1 second after the run returned, and it forces exit 10 seconds later.

What kept it alive was WPF's own exit handling. Each STA test thread that uses a WPF weak-event manager (a bound grid, say) makes WPF create a weak-event table. The table registers a process-exit handler.

At exit that handler runs on the main thread and finds the table's own thread gone. It then asks the dead dispatcher to purge the table, with a 300 millisecond timeout nobody answers. That costs about 0.3 seconds for every such test thread, after the run returns. About fifty test files start their own STA thread, so the exit stage grew past the 11 seconds the runner allows.

WPF has a runtime switch for exactly this case (Switch.MS.Internal.DoNotInvokeInWeakEventTableShutdownListener), which makes the handler purge the table directly. Both test projects now set it in their runtime configuration.

New tests: WpfExitSwitchTests runs in both test processes. It checks that the switch is on and that the installed WPF still knows its name. It also checks that a weak-event table's exit handler for an ended STA thread returns at once instead of after the 300 ms wait. WpfExitSwitchPinTests checks that both project files declare the switch. This is a CI-only fix: no product code changed.

Fixes #5596
…ry service start (#5597) (#5599)

The "Collection Falling Behind" alert no longer fires in the first hour after every service start. It leaves out what was counted in the first 15 minutes after the service starts, whichever way the clock steps. It does not fire or resolve until it has 15 minutes to judge. A store that stays behind still fires about 30 minutes after the start. Only a service start gets those minutes, once per process, not a stall, a pause ending or the memory launch guard's release. Slots that came due while a server was still seeding its schedule are no longer counted as skipped.

Fixes #5597
…s logs (#5587) (#5591)

A hung test shard names its test and keeps its logs (#5587)

A Darling PostgreSQL test shard ran into the job's 30-minute timeout and lost everything. GitHub left the later steps pending, kept no log and uploaded no timing. The build workflow's Darling and Lite test steps now have their own 22-minute timeout. So the upload steps still run when a shard hangs. The runner reports any test that has run for 5 minutes, and its output is uploaded whatever the step did.

The nightly run gets the same report and uploads. Its Lite step gets a 140-minute timeout. Its Darling step gets none, because its longest recent run takes 84% of the time a timeout leaves it. One test (RollupBackfillLiveTests) waited up to an hour while holding a lock. It now gives up after 60 seconds and fails by name.

Fixes #5587
…only that counter's data (#5574) (#5579)

* Perfmon data is compressed per counter, and chunks compressed by server alone are re-grouped (#5574)

New perfmon_stats chunks compress by server_id and counter_name. An hourly pass re-groups the
already compressed chunks inside the 30-day trend reach, newest first, one transaction per chunk
under the bounded lock timeout, for at most two minutes a pass. Below the floor (no per-chunk
settings view, TimescaleDB 2.14.1) nothing changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vDtCAiuajanNsX2cbhnwg

* Perfmon re-group: readers are never blocked, and a background loop converges the 30-day reach in hours (#5574)

The re-group now decompresses a chunk in one transaction and compresses it in a second, so the ACCESS EXCLUSIVE lock that decompress_chunk takes at its end is released before the compress half; reads of the chunk no longer wait for the rewrite. The work moves from a 2-minute slice of each hourly pass to a background loop (PerfmonRegroupDrain): one chunk at a time, newest first, a pause after each chunk as long as it took (at least 15 s), a busy lock skips the chunk for the run, three failures in a row stop it, and it ends on its own once nothing is left.

* perfmon re-group: live tests for the crash between halves, the pause, a busy lock, repeated failure, the compress retry, the plan shape, a concurrent reader and a service stop (#5574)

* perfmon re-group tests: hold a chunk's lock through a disposable (live-cleanup ratchet), reader bound 2.5 s (#5574)

* perfmon re-group: pause for the time a busy lock cost, 30-minute statement cap, remember done and capped chunks, honest run-end reasons (#5574)

* perfmon re-group: server cancel logged as such with its real time, gone chunk at either half confirmed by the chunk catalog, sturdier reach and reader tests (#5574)

* get_perfmon_trend reaches 30 days on the web and MCP; the two timed re-group tests run in the timing collection (#5574)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…up, or is refused up front (#5588)

On a large store, a one-day Custom Views panel on the Query Store wide table read 8.46 M rows. Cold, it took 55.2 s against the 60 s statement timeout, so a slightly larger store or a busier hour timed out.

Refusal. Before it runs a Query Store panel, the read guard estimates the rows it will read. The limit is 6.8 M rows for one scan and 5.4 M rows for two. Above it, the panel is refused up front with the reason, rather than running into the timeout.

One scan or two. A ranked time series panel reads the wide rows once only when time buckets times group members stay at or below 1.0 M. Above that, one scan risks the 1 GB temp file limit.

The server count is exact. For the database and module dimensions the count comes from planner statistics. Those run low on a large store (module names were 1.7 times low), so the count is multiplied by 4. A query hash ranking always takes two scans: on the large store, one scan of it wrote about 1 GB of temp files.

The hourly rollup (schema V173). The wide table gets an hourly rollup at the grain of each collection. It has one row per collection time, server, database, module and query hash, with the numeric sums a panel needs. The service builds an hour about 2 hours after it ends, up to 6 hours a tick.

Two triggers on the wide table mark an hour stale when a row lands in it after it ended an hour ago or more. The service then rebuilds it. The build of an hour waits while a write transaction that began early enough to touch it is still open.

Reading. A panel reads built hours from the rollup. It reads stale hours and the newest hours from the wide table. It returns exactly what the wide table alone returns. Live tests compare the two answers value for value, for every measure, aggregate and panel mode. The read guard counts a rollup hour at 0.3 of a wide hour, and a field run after deploy checks that weight.

The triggers cost about 13% more time per write for rows of the current hour on a test server. Rows that fire them cost up to about 55 ms more per 10,000 rows.

Upgrade. V173 adds the triggers to every partition of the wide table. If an anti-wraparound autovacuum is running on a large partition, the upgrade waits for it, and collection waits with it. On the large store the wide table was far from one: its transaction age was about 40 M, and that vacuum starts at 200 M. A migration step that hits its lock timeout now logs who holds the locks. It lists each session with a write lock or stronger on the store's tables. It logs at most every 10 minutes.

Part of #5582. Its round-2 fixes follow in their own pull request.
…TC (#5609)

The newest-snapshot block-count test seeded its window at the wall clock. Between 00:00 and 04:00 UTC that window crossed a day chunk boundary. The blocks the newest read touches then swung from 13 to 138, against a limit of 88. The test failed on every pull request whose Darling PG shard ran in that window. An earlier live test failed the same way between 00:00 and 03:00 UTC, so the fix covers the whole class of tests, not one.

A new LiveClock test helper gives two things. AnchoredEnd() is a fixed point inside one day chunk, 22:00 UTC on the previous day. Now() is the clock with a test-only override, the DARLING_TEST_CLOCK_UTC environment variable, which only the tests read. The machine clock is never changed.

Two audits covered 68 live test files that read the wall clock and use EXPLAIN or the chunk catalog. Each file now does one of three things. Its seeded window ends at AnchoredEnd(). Or its clock reads go through Now() where the test passes that time to the product. Or its assertion holds for any chunk layout.

A file whose product read takes its own clock stays on the real clock, with the reason written down. The force-plan failures test is one of these. Its read builds a two-hour window from the product's own clock, so a seed taken from the override fell outside it.

A new census, LiveClockCensusTests, has two rules. A live test that counts plan blocks, or uses EXPLAIN or the chunk catalog, and reads the wall clock without LiveClock fails. Each exception is on an allow list with its reason, and a stale entry fails. Run against dev without these changes, the census names 34 files.

The fixed classes passed with the override at 00:30 UTC and at 12:00 UTC and with it unset. The guards still fail on the defects they were written for. This is a test-only change.

Fixes #5608
…ble, and the migration lock log lists every lock holder (#5610)

The hourly Query Store rollup from #5588 held back its builds while any transaction in the database that had written something was still open. One long unrelated write stopped the builds, and panels went back to reading the wide table.

The build wait now counts only sessions that hold a write lock on the wide table or one of its partitions. A writer whose start time cannot be read counts as open. The plan skips the hours such a writer holds back, so they take no build slots and older hours still build. The wait can repeat on two ticks in a row, or a writer can stay open for over an hour. Then the service logs a warning with the session's pid, start time, application name and backend type, at most every 10 minutes.

A schema upgrade step that cannot get its table lock in time logs the sessions holding locks on the store's tables. That list now includes sessions that only read a table, because a plain read also blocks most schema changes. Each entry lists pid, backend type, application name, state, transaction start, wait event, table and lock mode. For an autovacuum worker it adds the first 200 characters of its query, which shows an anti-wraparound vacuum. The lookup has its own 5 s statement timeout and 2 s lock timeout. A failed lookup no longer mutes the next 10 minutes, and a shutdown during the lookup stops it.

Fixes #5582
…get and continues within the hour (#5595)

On a large store, the first retention drain after the 3.10 upgrade deleted about 18 M rows in 40 minutes and paced only its WAL. Collection fell behind while it ran, and servers skipped collection slots.

Every batched delete now goes through one gate. Before each batch, the drain waits while collection is behind, re-checking every 5 s. Collection is behind for the first 10 minutes after the service starts collecting. It is also behind when at least 3 slots were skipped in the last 10 minutes. Those must be at least 2.5% of the slots that came due.

A pass spends at most half its budget waiting. After each batch it pauses for as long as the batch ran, at most 60 s.

A pass stops at the next batch boundary once it has run 30 minutes, waits included. The run record says where it stopped and how many tables it did not reach, and the status stays SUCCESS. The next pass then comes due within an hour of the stop, not a day later, until a pass drains every table. This applies to the daily purge and to purge_now.

The rule was retuned after a read of a normal day. A synthetic small-store hour reads behind in none of 60 checks. A replay of the large store's backlog kept p95 lag at 0.99 to 1.01 times the phase with no drain. The wide-table drain's p95 write latency was 0.44 times.

Fixes #5592
…#5605)

The live aggregate quiesce tests no longer need to see a TimescaleDB job worker alive inside a time window. The scheduler ends a worker whose job was stopped at its next wake. On a loaded machine that can be within milliseconds of the stop.

The hold test and the at-the-cap test give their gate function a trap. The trap counts the scheduler's cancel and keeps waiting, so the worker survives 3 s or more after it. The hold test waits for that count instead of for two seconds. Both tests also check that no cancel came from anywhere but the scheduler.

The cancel test polls every ten minutes, so the sweep can never reach its second look before the test cancels it. The comments on the quiesce sweep give the measured scheduler timing.

Without the trap, a cancel ended the worker at once with an `invalid transaction termination` error in the PostgreSQL log. That is how the CI failure looked.

Fixes #5603
…y the pull request's own change, and the shadow check reads Guard classes and the latest attempt (#5459) (#5611)

CI's test map now links every class of a shared live-store test collection to the files its fixture ran. This fixes the 11 change-related misses among the 55 replay misses. The other 44 misses are unrelated to the change and are listed in the accepted file.

A keep-full file now counts only when the pull request itself changes it. The FULL rows among the 76 shadow rows go from 68 to about 47.

The shadow check now reads the reports from the Guard tests job. For each downloaded artifact, it keeps only the newest attempt.
The shard timing uploads now set overwrite, so a re-run replaces the old artifact whole.

Refs #5459
…splice (#5613)

This adds fourteen entries for the PRs that merged to dev after the third splice: two Added, three Changed and nine Fixed. Each full entry goes into the 3.10.0 archive, its title-only index line goes into the CHANGELOG.md index, and each reference gets a link definition. The archive census is regenerated.

The four modes of the changelog archive script all succeed. The changelog, version, release gate, census and guard test classes pass.

Changelog entries for the 3.10.0 PRs merged after the third splice.
) (#5612)

* Lite tests copy a prebuilt database instead of rebuilding the schema under the shared write lock (#5208)

* Lite test copies of the prebuilt database get their own store identity, and 63 more test files use the copy (#5208)

* Keep the real InitializeAsync in tests that reset, restart or reopen an existing file (#5208)

* InitializeFromTemplateAsync is as safe to call again as InitializeAsync (#5208)

A repeat call on a path that already holds the store now runs the real
InitializeAsync instead of copying over it, so a helper that initializes once per
call no longer throws 'already exists' (ArchiveGroupedWatermarkReadTests, 38 tests).
A changelog splice changes CHANGELOG.md, docs/changelog/<version>.md and tools/changelog/archive-census.txt. The census is a text file that was not on the documentation list. So the splice ran about 17 minutes of Guard tests and whole-tree guards instead of the docs fast path. The exact census path is now on the list in both copies in build.yml and in ci-select.py. A test pins that a splice is docs-only and that the three lists agree. The CI replay accepts one past splice run, on which an unrelated live-database class failed.

What this gives up: ChangelogIndexAndArchiveTests no longer runs on a splice pull request, as it already did not run on a CHANGELOG.md-only change. The splice lane runs it before it pushes, and the nightly runs the full suite.

Fixes #5614
…k log writes by design (#5619)

#5610 added a check to MigrationLockHolderLogTests that no part of the migration lock log line contains "query:". The log adds the first 200 characters of the query for an autovacuum worker on purpose. The test passed review and CI because most runs have no autovacuum worker holding a lock on a darling table. When one did, the test failed the pull request.

The test now splits the line into one entry per session. The entries for its own holder and reader must exist and carry no query text. Any entry that carries query text must be an autovacuum worker's. The checks that the two sessions' statements never appear stay on the whole line. No product code changes.

Fixes #5617
…est database (#5618) (#5621)

A Darling live test ran the managed provisioning batch inside a scratch database. That batch revokes CONNECT from PUBLIC on the database named darling. Database privileges belong to the whole cluster, so every later test that relied on PUBLIC's CONNECT there failed with permission denied. The password-key test now points its database-level statements at its own scratch database. The redaction test grants its own login CONNECT instead. A new guard on the cluster-roles test collection fails the test that leaves PUBLIC without CONNECT, by name, and no product code changes.

Fixes #5618
…#5208) (#5622)

Many Lite test classes were slow because they queued behind other tests that held the one database write lock. The copy step that builds each test store, and helpers that rebuilt the same store on every call, held it longest. The copy step now opens its connection and renews the copy's identity first, so the lock is held only to swap the connection in. The helpers build a store once, and a race test writes 1,000 keys instead of 4,000 but still fails without the write lock. Four groups that stand in for the CI shards ran in 45 to 80 s instead of 106 to 119 s on a laptop. WebhookCooldownSeedTests now posts to an in-process endpoint instead of a closed port, so it takes 0.2 s instead of 20.5 s.

Fixes #5208
… so the nightly publishes sooner (#5620)

The nightly ran each test suite in one step, so its publish job landed about 50 minutes after the run started. PR CI runs the same suites in shards in about 19 minutes.

The cut, the Cost=Slow skip, the chunks and their runs moved from two build.yml steps into two scripts, .github/scripts/run-darling-pg-shard.ps1 and .github/scripts/run-lite-shard.ps1. build.yml and nightly.yml both call them, and PR CI's cut is unchanged. The nightly's Darling PostgreSQL job is now six shards with the same test set as before. A new Lite job runs four shards, with every class, the Guard classes too. The build job no longer runs tests, and its timeout is back at 90 minutes.

Publish now waits for every shard of both suites, and runs only on the dev ref. The publish input stays as a second gate. The test map and AltCover jobs also wait for the Lite shards. The shard scripts and the Lite packer are in the path filters and the slow-skip build inputs, so a change to one runs the suites.

A proof run from the branch had everything publish needs done 16.6 minutes after it started. The last three dev nightlies took 44.8 to 49.4 minutes. The legs' class counts sum to the whole suites.

Fixes #5616
…5623)

The nightly's seven checkouts used the branch name. Each job checked out the branch tip at the moment it started. So a merge to dev during a nightly changed what later jobs built, the shards and publish among them. Dev had to freeze during every run.

Every checkout now uses the run's commit (github.sha), the dispatched branch's tip at dispatch time. A branch dispatch still builds its own branch, and the scheduled path still builds dev's tip from when it was dispatched. NightlyCheckoutCommitPinTests (a Guard class) fails if any nightly checkout goes back to a branch name, or if the redispatch job gains a checkout.

Fixes #5616
…#5624)

A pull request now runs only the test classes that the nightly test map picks for its diff. The Darling PostgreSQL shards, the Lite shards and the build job's Darling pass keep only those classes, and the Guard stage still runs in full. If the selection is full, missing, unreadable or empty, they run everything. Pushes, merge-queue runs and the nightly always run everything.

Replaying past failures against the map found six misses. None was caused by its pull request's change, so each is listed as accepted with the evidence, and the replay now finds none.

A class gated on a PostgreSQL variable that only CI sets is always kept. A nested class is matched by its own name.

A pull request into main, the release pull request, always runs every class, because the selection and the Cost=Slow skip both check the base branch.

Fixes #5459
…s in bounded pages (#5625) (#5626)

Darling's hourly maintenance step over stored analysis findings no longer times out on a large store. It reads a fixed range of the findings table per page. A page costs the same whatever the table's size.

The two stages over stored alerts now read one server at a time through the alert log's existing index. Deadlock alerts are paged per server, and finding alerts are paged per server and metric. Each page is a range of that index. That bounds it by its size on PostgreSQL 16, 17 and 18 alike. The report lookup uses the existing index on stored reports. No store migration is needed.

Live tests at about 500,000 alert rows check the plans, the rows read per page, and that every raw row is rewritten once.

Fixes #5625
…ng install gets exact steps (#5628)

- The pre-lock check trusts the product's own service account only in the folders and files the service writes.
- One shared list names those places, and the install lock and the check both read it.
- The refusal for an install whose service runs from the refused folder gives numbered steps to move to a new folder.
- A service registered elsewhere, or none, gets the fresh-folder wording.
- Paths darling.json names inside the old folder are listed, quoted and capped, and the old folder is kept until they move.
- upgrade-darling.ps1 applies its -Source default in the body, so it runs on Windows PowerShell 5.1 from a staging folder.
- A new CI check runs both scripts over real 3.7.1 and 3.9.0 installs, and checks each release zip against the release's checksum file.
- The printed by-hand fix grants the service read and execute on the install folder.
- The compose quickstart pulls the :latest image.

Fixes #5627.
Fixes #5629.
…r a failed pass (#5637)

Darling's hourly PostgreSQL deadlock maintenance logged a warning when a stage failed a pass. It logged nothing when a later pass succeeded, so the last line an operator saw was the failure. A stage that succeeds after one or more failed passes now logs one information line that names the stage and how many passes had failed. A clean pass with no earlier failure logs nothing new, and the back-off and skip logic are unchanged.

Fixes #5634
…ion-test errors carry less detail; token honored on loopback listeners (#5636)

The Darling web dashboard now sends four response headers on every reply: a content security policy, X-Frame-Options, X-Content-Type-Options and Referrer-Policy. The sign-in and landing pages carry a policy that names the hash of their own inline blocks.

The confirmation shown before a captured query is re-run for its actual plan now adds one sentence. It says the text was captured from the monitored server. The Darling Viewer, Lite and the shared plan history windows show the same sentence.

A failed connection test from an MCP tool or a web route now answers with one fixed sentence naming the host and port. The driver's text goes to the service log on one bounded line, with the typed password and AWS external ID redacted. The host command line and the desktop Viewer's Add dialog keep the detail.

A token set in mcp.network or web.network now gates the loopback listeners too. Local MCP clients send an Authorization header, and local browsers sign in with the token. With no token configured nothing changes. A token that is set but blank or cannot be decrypted stops that listener from starting, in loopback-only and network mode alike.

The Darling MCP server answers 415 to a POST that does not carry a JSON Content-Type. The check runs after the Host, CIDR and token gates, so a caller that fails a gate gets 403 or 401 first. The service log records each 415 the way it records the other refused requests. The Viewer's copy-MCP-command button now tells the user how to add the token header.

Lite's MCP server answers 415 to a POST that does not carry a JSON Content-Type in the same way.
Adds the changelog entries for #5626 and #5628 to the 3.10.0 index and to docs/changelog/3.10.md. The #5628 entry cites its own PR. The 3.10.0 date is now 2026-10-09. A second Important entry covers the Windows upgrade stop for installs that ordinary users can write to.

The Important note now names store migrations V158 through V173, as dev is at schema 173. The README gains the shared time range picker, the text column filters and Lite's DuckDB memory limit setting.

The branch merges dev, which brings in the entries for #5636 and #5637, and every entry from both sides is kept. The archive split and census run last. The 3.10.0 census row is 295 entries, and verify and roundtrip both pass. No duplicate section headers and no missing reference links were found in the 3.10.0 entries.

The last changelog splice for 3.10.0, and the README sync.
@erikdarlingdata
erikdarlingdata merged commit 10227c2 into main Oct 9, 2026
71 of 72 checks passed
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.

2 participants