Skip to content

Release checks: small fixes from the final walk of Lite and the Darling Viewer - #5542

Merged
erikdarlingdata merged 30 commits into
devfrom
fix/release-walk-1008
Oct 8, 2026
Merged

erikdarlingdata merged 30 commits into
devfrom
fix/release-walk-1008

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Small fixes found in a final walk of Lite and the Darling Viewer before release.

Collector Schedules preset and status agree

Finding. Settings > Collector Schedules in Lite showed Preset "Custom" and Status "Default" on every server row.

Cause. The two columns answer different questions, and both were true. Status ("Default" or "Customized") only said whether the server has its own schedule or follows the default schedule. Preset said which preset's intervals the schedule in use matches. On the walk machine no server has its own schedule, so each row follows the default schedule, and that saved default schedule still holds deadlocks at 1 minute (the interval before it became 5), so it matches no preset and reads "Custom". I compared the saved file with the Balanced preset read-only: deadlocks 1 vs 5 was the only difference, and there are no server overrides. A fresh install reads Balanced. Nothing in the preset matching was wrong (the preset table and the shipped defaults agree), so I kept the pairing and made it say what it means.

Fix. ScheduleManager.GetServerScheduleSummary (Lite/Services/ScheduleManager.cs) returns Preset, Status and a tooltip from one read of the schedule list the server runs on, and the Settings window uses it for all three, so the columns cannot come from different lists. Status is now "Uses default" or "Own schedule". Both column headers carry a tooltip saying what they mean. The Preset cell tooltip says "Every collector interval matches the Balanced preset." or, for Custom, names the closest preset and up to five intervals that differ (for example deadlocks 1m (Balanced 5m)). Files: Lite/Services/ScheduleManager.cs, Lite/Windows/SettingsWindow.xaml, Lite/Windows/SettingsWindow.xaml.cs.

Darling twin. The Viewer has no Preset/Status grid. Its Settings window opens CollectorScheduleEditorWindow, which has a Scope box, a "Use default schedule" check box and a Preset box for the list being edited, each with its own label. I checked Darling/PerformanceMonitor.Darling.Viewer/CollectorScheduleEditorWindow.xaml and CollectorSchedulePresets.cs, and searched Darling for the preset names; there is no web Admin collector-schedule preset view. Nothing to change there.

Test. Lite.Tests/ServerScheduleSummaryTests.cs (6 tests): shipped defaults read Balanced / "Uses default"; an old deadlocks interval in the default schedule reads Custom / "Uses default" with the interval named in the tooltip; a server with its own Low-Impact schedule reads Low-Impact / "Own schedule"; one changed interval on an own schedule reads Custom / "Own schedule"; the tooltip lists five differences and counts the rest; and the Settings window loads all three from the one summary.

RED. With the status words put back to "Default" / "Customized" and the Settings window back on the two separate reads: Total 6, Failed 5. GREEN. Total 6, Failed 0.

Other classes (final head, Lite.Tests, 0 build warnings). CrossAppPresetValuePin Total 2 Failed 0; SharedCollectorDefaultsPin Total 9 Failed 0; *Census* Total 58 Failed 0; *Scrub* Total 25 Failed 0; *Guard* Total 124 Failed 0; *Ratchet* and *Hygiene* match no class in Lite.Tests (Total 0). Darling.Tests not run for this section (no Darling file changed).

Not done. None. The saved default schedule on a machine that kept an older interval is left as the user's setting; it is not rewritten.

Server Inventory and Utilization show one health score

Finding. In Lite, the FinOps Utilization tab showed Health 99 for one server and 85 for another, while the Server Inventory Health column showed 93 for all five. One server had two scores, and five different servers shared one number.

Cause. The two tabs used different rules. The Utilization badge scores the 24-hour p95 CPU, the buffer pool's share of physical memory, and the free share of the newest database sizes. The Inventory grid scored the 24-hour AVERAGE CPU and two fixed values for memory (80) and storage (50 percent free), because its read did not have the buffer pool or the file sizes. Servers with similar CPU therefore all landed on the same number. The Darling Viewer, the get_finops_inventory tool and the web Server Inventory page had the same defect through FinOpsInventoryFigures.HealthScore, so they got the same fix.

Fix. One rule, FinOpsHealthCalculator.Score in Lite (Lite/Services/LocalDataService.FinOps.cs) and FinOpsUtilizationFigures.HealthScore in Darling. The fleet read behind the Inventory grid (GetServerMetricsAsync in Lite, DarlingFinOpsInventoryReader.ServerMetricsSql in Darling) now also reads the newest memory row's physical memory and buffer pool and the newest size snapshot's allocated and free totals, and computes the score from them over the same 24-hour window as the Utilization read. The old defaults-based InventoryScore and FinOpsInventoryFigures.HealthScore are removed. The dash when there is no CPU sample stays; a server with no memory row also reads a dash, because the Utilization tab has no card for it either. Darling's get_finops_inventory description now says the score is the Utilization view's score. Files: Lite/Services/LocalDataService.FinOps.ServerProperties.cs, Lite/Services/FinOpsServerInventory.cs, Lite/Controls/FinOpsTab.xaml.cs, Darling/PerformanceMonitor.Darling.Storage/FinOps/DarlingFinOpsInventoryReader.cs, FinOpsInventoryFigures.cs, the Viewer loader and row types, and DarlingMcpFinOpsInventoryTools.cs.

Tests. Lite: FinOpsFleetReadParityTests.ServerMetrics_HealthScore_EqualsTheUtilizationScore_PerServer seeds two servers with different CPU, memory and storage and checks the fleet score equals the Utilization score for each, and that the two differ. Darling (live PostgreSQL): FinOpsInventoryHealthScoreLiveTests.InventoryHealthScore_EqualsTheUtilizationScore_PerServer, same shape. Pins that named the old defaults-based call (FinOpsIdleCoverageAndInventoryScoreTests, AzureSqlDatabaseOwnFiguresTests in both projects, ViewerFinOpsInventoryDelegatesTests, FinOpsInventoryViewerFiguresTests, FinOpsClickThroughRulesTests, the inventory tool guide-head pin) now pin the new call. The inventory golden fixture gains the HealthScore field, regenerated on purpose. The inventory tool's parity test now seeds a memory row, since a server without one has no score.

RED. With the fleet score put back to the old average-CPU-plus-defaults formula: Lite FinOpsFleetReadParityTests Total 7, Failed 1; Darling FinOpsInventoryHealthScoreLiveTests Total 1, Failed 1. GREEN. Both Total 7 / Total 1, Failed 0.

Other classes (final head, 0 build warnings in both test projects). Lite.Tests *FinOps*, *AzureSqlDatabaseOwnFiguresTests*, *Census*, *Ratchet*, *Hygiene*, *Scrub*, *Guard* in one run: Total 406, Failed 0. Darling.Tests on a local PostgreSQL with TimescaleDB: *FinOps*, *Census*, *Ratchet* Total 1295, Failed 1 (the inventory tool guide-head pin, fixed afterwards and re-run: Total 80, Failed 0 with DocCommentHygieneTests); *McpToolGuideHeads*, *Hygiene*, *Scrub*, *Guard*, *DarlingMcp* Total 2034, Failed 1 (a stacked doc comment in my Lite test file, fixed and re-run: Failed 0).

Not done. None. The Darling web page reads the same tool rows, so it needed no separate change.

Viewer fleet screens: dates, clocks, counts and stale marks

Five findings from the walk of the Darling Viewer, with the Lite twin checked for each.

D4. Overview card "Last Collect" showed only a time for a collection from days ago. Cause: the Viewer's ServerSummaryItem.LastCollectionDisplay always formatted HH:mm:ss; Lite's card already adds the date when the collection is not today. Fix: ServerSummaryItem.FormatLastCollect (Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.Overview.cs) uses HH:mm:ss when the wall time is today in the display zone and yyyy-MM-dd HH:mm:ss otherwise. Lite: checked LocalDataService.Overview.cs, it already does this. I changed ViewerW2aTests.LastCollectionDisplay_TreatsStoredValueAsUtc_ShownLocal to expect the date, because its stored time is from July.

D5. Alert History and Job History times mixed clocks with no label. Cause: in Server mode each Alert History row correctly converts on its own server's clock (a server with no collected clock stands in with the machine's offset), but nothing on the screen said so, and Viewer Job History rows converted on whichever server tab was active (the machine's clock when none is open). Fix: the Alert History "Time" and Job History "Run Time" headers now read "(server)", "(local)" or "(UTC)" after the "Show timestamps in" choice (PerformanceMonitor.Ui/TimeColumnTitle.cs, set on every load), in Lite and in the Viewer. Viewer Job History rows now carry their own server's clock (ViewerJobHistoryRow.Clock, stamped in GetJobHistoryAsync from the same cached fleet clocks Alert History uses). A judgment call: in Server mode a list of several servers still shows each server's own hour, which is what that mode means; Local and UTC are one clock for every row. Lite: its rows already convert per server (collected clock, else open tab, else machine); only the headers were missing.

D7. Alert History said "500 alert(s)" at 24 hours and 7 days with no note that the read is capped. Fix: JobHistoryCap.CountText(shown, read, cap, noun) (PerformanceMonitor.Common) now also builds the alert text, "500 alert(s) (showing the newest 500)" when the read reached the cap, whatever a column filter then leaves on screen. The cap is one constant (AlertsHistoryTab.RowCap) used for the read and the label, in the Viewer and Lite. The Job History wording is unchanged.

D9. The Overview search box left the Needs Attention rows and "Monitoring N servers" alone. Fix: OverviewCardView.BuildRollup rebuilds the roll-up from the cards the search leaves; the subtitle reads "Monitoring 2 of 9 servers" while filtered (FleetRollup.FilteredOfTotal). MainWindow.ApplyOverviewCardFilter re-applies it on every keystroke from the held totals, with no extra store read. The needs-attention toggle does not narrow it (the panel is the shortlist that toggle opens). Fleet-wide blocking and deadlock totals stay fleet-wide. Lite has no fleet roll-up panel (searched Lite/MainWindow.xaml*).

D10. The Availability Groups tab showed a green "Healthy" badge on 13-day-old data. Fix: AgTopology.BuildCards takes the clock and marks a card whose newest replica snapshot is older than ServerHealthThresholds.OfflineThreshold (the 30 minutes the web page's stale rule uses) as IsStale: label "Stale", band at least Warning (never Unknown, which sorts below Healthy). The stale flag is in the refresh digest and copied by UpdateFrom, so a card that goes stale with unchanged rows is redrawn. Viewer and Lite both pass the clock (LocalDataService.AgTopology.cs). I did not copy the web page's "no primary reported is never Healthy" rule: a secondary-only reporter legitimately sees no primary.

Tests. Darling.Tests/ViewerFleetScreenWalkFixesTests.cs, 19 tests, no window. RED: with the production lines reverted one at a time (always-today, search ignored, Clock ignored, stale never set, cap label off) Total 19, Failed 11; the other 8 are new-API or header-text checks that do not compile on the old shape. GREEN: Total 19, Failed 0. Final head after merging this branch and dev, both test projects 0 build warnings: Darling.Tests *Census*, *Ratchet*, *Hygiene*, *Scrub*, *Guard*, the new class, *ViewerW2a*, *ViewerFleetRollup*, *ViewerOverviewExplainsItself*, *AvailabilityGroups*, *JobHistory*, *ViewerAlertRowClock*: Total 1163, Failed 0 (98 skipped, live PostgreSQL tests). Lite.Tests *Census*, *Ratchet*, *Hygiene*, *Scrub*, *Guard*, *JobHistory*, *AgTopology*: Total 269, Failed 0. One run first failed TsqlConventionGuardTests.TheMemberScan_ReadsEveryDeclarationWhole on my MonitoringText (is { } pattern); I rewrote it with HasValue and it passes.

Not done. None. The Lite-side headers, cap note and stale AG mark have no Lite-only test; the shared helpers they call are covered by the Darling tests above.

Screen readers name grid headers, filter buttons, blank cells and server list items

Finding. Read through UI Automation, Lite's Alert History and Job History column headers were named "System.Windows.Controls.StackPanel" and their filter buttons had no name (a bare U+E71C glyph). The Viewer had the same in Alert History, Job History, FinOps Recommendations, Storage Growth and every other grid with a panel header. Lite's Recommendations server box had no name and its selected item read "ServerConnection". In the Viewer, FinOps server box items read "...DarlingServer", Overview cards read "...ServerSummaryItem", Needs Attention rows read "...FleetRankedServer", and an empty Retries cell in Job History read "Item: ...ViewerJobHistoryRow, Column Display Index: 7" (Lite's Job History has the same empty Retries cell).

Cause. Two causes. (1) The shared AccessibleNames class already named tabs and column headers, but it hooked Loaded with a class handler. WPF raises Loaded straight to an element's own handlers and never runs class handlers for it, so the handler never ran and every panel header kept its type name. I confirmed this with a real grid in a window: the header item peers read "System.Windows.Controls.StackPanel" and the buttons read "". Hooking SizeChanged (a real routed event, first raised at the first layout) names them "Time", "Filter Time". (2) The server box items and cards are named by UI Automation from the item's ToString(), which was the type name; Lite's Recommendations box also had none of the name and item-name setters that Lite's FinOps box got earlier. A cell with empty text is named by WPF from the row's type name.

Fix. PerformanceMonitor.Ui/AccessibleNames.cs: tabs and column headers are named on SizeChanged (skipped once a watcher keeps the name current), and an empty text cell is named ": blank" and loses that name when its text fills in; a name set by hand, or a cell with text, is untouched. ToString() returns the server's name on ServerConnection (with the "(Read-Only)" suffix the box shows), Lite's ServerSummaryItem, and the Viewer's DarlingServer, ServerSummaryItem and FleetRankedServer. Lite's Recommendations box and the Viewer's FinOps and Recommendations server boxes get AutomationProperties.Name="Server" and an item container style naming each item. The Viewer shares AccessibleNames (its App.xaml.cs already registers it), so the header, filter button and blank-cell fixes reach every grid in both apps with no per-grid change. Same layout, glyphs and text on screen.

Tests. Lite.Tests/ScreenReaderNamesTests.cs and Darling.Tests/ViewerScreenReaderNamesTests.cs, 7 each. Each opens a real grid in a window and walks the automation tree (header items, filter buttons, blank cells), checks the item types' names through a list's item peers, and runs a census of the app's XAML: every panel column header has a visible title to name it from, every filter button sits in a column header, and the server boxes carry the name and item style. RED: production files put back to the previous head: Lite.Tests Total 7, Failed 4; Darling.Tests Total 7, Failed 5 (the two census tests pass on the old XAML by design; they guard new headers). GREEN: Total 7, Failed 0 in each. Final head after merging this branch and dev, 0 build warnings in both projects: Lite.Tests *ScreenReaderNames*, *ServerTabEmptyStateAndNames*, *Census*, *Ratchet*, *Hygiene*, *Scrub*, *Guard*: Total 241, Failed 0. Darling.Tests *ViewerScreenReaderNames*, *Census*, *Ratchet*, *Hygiene*, *Scrub*, *Guard*: Total 946, Failed 0 (58 skipped, live PostgreSQL tests).

Not done. Nothing here. The "Screen readers name grid rows" section below covers grid rows and templated cells.

FinOps Utilization: dashes for missing CPU data, and Storage Growth baselines checked

Finding D6. The FinOps Utilization view for a server with no CPU sample in the last 24 hours (a stale server) showed "No Data" and "Health: -" (right) but Avg CPU 0.00%, P95 CPU 0.00% and Max CPU 0%, and still drew the Database Sizes (Allocated vs Used) chart from that server's latest, possibly days-old, snapshot.

Cause. The read returns 0 for the three CPU figures when the window holds nothing, and the tab printed them as measured values. Only the no-row case and the verdict/health badge knew about the missing sample. The sizes chart was drawn whenever the Utilization row existed.

Fix. Each app's UtilizationEfficiencyRow (Darling Viewer: ViewerDataService.FinOps.cs; Lite: Lite/Services/LocalDataService.FinOps.cs) gets AvgCpuText, P95CpuText and MaxCpuText (a dash when HasCpuSample is false, the figure otherwise, so a measured idle 0 still reads 0) and ShowsDatabaseSizeChart (false with no CPU sample). The Viewer's FinOpsTab.Loaders.cs and Lite's FinOpsTab.xaml.cs paint the three texts from the row, leave the three CPU bars empty with no sample, and collapse the sizes chart group (named FinOpsDbSizeChartGroup / DbSizeChartGroup in the two XAML files). The chart is hidden rather than labelled, because the read carries no collection time for it. The Database Sizes sub-tab, which shows the rows themselves, is unchanged. The web Utilization page already shows the snapshot time on its own Database Sizes panel; its CPU figures come from the utilization MCP read, which now returns null (not 0) for cpu.avg_cpu_pct, p95_cpu_pct and max_cpu_pct when there is no sample (DarlingMcpFinOpsTools.Utilization.cs). The same file's tool guide said the health score falls back to a memory and storage 30:30 split with no CPU sample; that was out of date since the shared health score change, so it now says the score is null.

Finding D8 (not a defect in the current code). Storage Growth "Size 7d Ago" and "Size 30d Ago" equal to current with growth 0.00 on a store holding about 6 days. I did not read that store. In the current code both the Viewer (DarlingFinOpsStorageGrowthReader.GetStorageGrowthAsync, which ViewerDataService.GetStorageGrowthAsync calls) and Lite (LocalDataService.StorageGrowthSql) take each baseline as the sample nearest its mark and within one day of it, strictly older than the latest sample, else null with "No sample from N days ago". The walk ran on f98ab41, which is the #5492 merge, so the code follows the rule and the new tests prove it. I did not check the walked values against the store: an unchanged database reads equal to current at every mark that has a sample. With 6 days of history the oldest sample is within one day of the 7-day mark, so it is a real 7-day baseline, and an unchanged database truthfully reads equal to current with growth 0. No sample can be within a day of 30 days, so the 30-day cells read n/a. I changed no code for D8 and pinned it with tests.

Tests.

  • Darling.Tests/ViewerFinOpsUtilizationNoCpuTests.cs (6): dashes with no sample, measured figures (including a real 0) with one, the chart flag, and the tab's painting pinned.
  • Lite.Tests/FinOpsFirstVisitAndBaselineTests.cs: new FinOpsUtilizationNoCpuTests (6, the Lite twin) and two tests in FinOpsStorageGrowthBaselineTests (a 6-day history gives a real 7-day value and n/a for 30 days; an unchanged database reads equal to current with growth 0).
  • Darling.Tests/StorageGrowthNoHistoryLiveTests.cs: a 6-day history on a test PostgreSQL server, same two cases.
  • Darling.Tests/FinOpsUtilizationViewLiveTests.cs: the no-CPU server's three CPU figures are null.

RED (production lines reverted locally, then restored): Viewer class Total 6, Failed 2 (the dash test and the chart test; the other four pin the tab source or measured figures, which the revert leaves alone); Lite class Total 6, Failed 2; the utilization MCP live test Total 1, Failed 1. The D8 tests pass on the unchanged code, which is the point: they show the rule works. GREEN: Viewer class 6/0; Lite class 6/0; FinOpsStorageGrowthBaselineTests Total 8 Failed 0; StorageGrowthNoHistoryLiveTests Total 4 Failed 0; FinOpsUtilizationViewLiveTests Total 12 Failed 0.

Other classes (this section's head, 0 build warnings in both test projects). Darling.Tests: *Census* Total 456 Failed 0; *Ratchet* 15/0; *Hygiene* 90/0; *Scrub* 120/0; *Guard* 336/0; *ViewerFinOps* 152/0; *DarlingMcpFinOps* 36/0; *FinOpsUtilization* 60/0; FinOpsTabUtilizationPageTests 10/0. Lite.Tests: *Census* 58/0; *Scrub* 25/0; *Guard* 124/0; *FinOps* 175/0; *Ratchet* and *Hygiene* match no class (Total 0).

Not done. The sizes chart is hidden, not labelled with its collection date; a label needs the snapshot time added to the size reads in both apps. Nothing was run in the apps themselves.

Screen readers name grid rows, blank templated cells, and the handlers cost nothing after the first look

This finishes the four items the section "Screen readers name grid headers, filter buttons, blank cells and server list items" left open. Its "Not done" note about grid rows and templated cells is replaced by this section.

Finding. Read through UI Automation, every grid row was named by its data item's type name ("...ViewerJobHistoryRow"), in both apps. A blank cell built from a template (not a plain text cell) kept the "Item: ..., Column Display Index" name.

Cause. Nothing named a DataGridRow, and the blank-cell rule only looked at TextBlock cells. Separately, the shared handlers ran on every SizeChanged; a header whose content has no text (so nothing to watch) was walked again on every resize, and a blank cell re-read its column title on every resize.

Fix (PerformanceMonitor.Ui/AccessibleNames.cs, shared by Lite and the Viewer; no per-grid code).

  • Rows: a class handler on DataGridRow names the row from the first two visible columns (in display order, hidden columns skipped) that have text: for example "Time 06:01:12, Server example-sql-01". Templated columns count: their text is read from the realized cell. Text watchers on the cell text blocks keep the name current; a row reused by a virtualized grid for another item is re-read through its DataContextChange (once, at Loaded priority, after its cells have taken the new values), and a row that is unloaded and put back is re-read on Loaded. A name set by hand always wins.
  • Templated cells: the blank rule now judges any cell by what it shows. It is blank when it holds only empty text blocks inside plain panels, borders and content presenters. A check box, button, image, shape, or a panel or border with a background counts as content and the cell is left unnamed. A blank templated cell is named ": blank" and loses the name when its text fills in or its content changes.
  • Cost: every per-event handler is now a property read and a return once the element has been looked at. Headers and tabs remember the header object they last checked (compared by reference); cells and rows keep an Active flag. Walks and watcher changes happen only on first look, on a text or content change, and on reuse.

ToString audit (the five overrides from the first pass). All five are classes, not records, so there is no generated ToString printing properties; each returns only the display name (Lite ServerConnection returns the name plus " (Read-Only)" when set). None includes a password, connection string or any other property, and the first-pass tests pin the exact text. Where each type can reach a string:

  • Lite ServerConnection, Lite ServerSummaryItem, Viewer DarlingServer, ServerSummaryItem, FleetRankedServer, interpolation and +: every site found formats .DisplayName, .Id or .ServerName explicitly (the {server} hits in EntraDeviceCodeAuth, RemoteCollectorService.LogWatermarkReadFailure and App are strings) - unchanged meaning.
  • Log templates ({Server} placeholders): the arguments are server.DisplayName; LogEnumerationProbeFailures(definition, server, ...) takes the object and logs server.DisplayName - unchanged meaning.
  • string.Join: only DescribeServer as an explicit selector in the Viewer's main window - unchanged meaning.
  • Convert.ToString, .Tag/.Content/.SelectedItem...ToString(): the hits read ComboBoxItem tags and content, which are strings - unchanged meaning.
  • Dictionary, HashSet or cache keys of these types: none exist, and Equals/GetHashCode are not overridden, so identity is unchanged.
  • File names, SQL and JSON: none take the object; JSON uses properties, not ToString.
  • XAML bindings without a path: two {Binding} header templates bind strings; the other lists use DisplayMemberPath.
  • Remaining effect: lists and drop-downs with no template, and list type-ahead, now use the server's name instead of the type name. That is the point of the override.
    The audit is by search of both apps; an attempt to prove it by making the overrides throw and running all of Lite.Tests stalled in an unrelated test after the first one (over 590 seconds), so it was dropped and the overrides were put back.

Tests. Lite.Tests/ScreenReaderRowNamesTests.cs and Darling.Tests/ViewerScreenReaderRowNamesTests.cs, 5 each, all on real DataGrids in a window:

  • a row's automation item reads "Time 06:01:12, Server example-sql-01" and no row type name;
  • hidden columns are skipped, templated columns are read, and an empty first column hands over to the next ones;
  • a recycling grid of 3000 rows scrolled from the first row to the last in steps: every realized row's name matches its own item, the watcher count never exceeds twice its starting value or 600, so it follows the realized cells and not the row count;
  • a templated cell with only an empty text block is "Retries: blank" and drops it when its text fills in; a templated cell with a check box and a text cell with text stay unnamed;
  • the cost: after one pass, 2000 passes of the four handlers over every tab, header, row and cell allocate 0 bytes and add no watchers.

RED (Lite; production lines reverted locally, then restored): with the row handler unregistered, the content-presenter branch of the blank rule disabled and the cell handler always re-reading, ScreenReaderRowNamesTests Total 5, Failed 5. With only the header and tab "already checked" test put back to the old watcher test, Total 5, Failed 1 (the cost test). The Viewer class shares the same production class; it was run GREEN only.
GREEN. Lite *ScreenReader* Total 12, Failed 0; Darling.Tests *ViewerScreenReader* Total 12, Failed 0.

Other classes (final head, 0 build warnings in both test projects). Lite.Tests *ScreenReader*, *Census*, *Ratchet*, *Hygiene*, *Scrub*, *Guard* together: Total 210, Failed 0. Darling.Tests, same filters: Total 951, Failed 0, Skipped 58 (the live tests that need a test PostgreSQL server, which was not started for this section).

Not done. A templated cell whose template swaps one element for another later (a trigger) is judged again only when its text or content changes. (The drag case is fixed in "After the code check: screen-reader names" below.) The Viewer's own RED run was not repeated; it uses the identical shared class. Nothing was run in the apps themselves.

After the code check: screen-reader names

Six small fixes to PerformanceMonitor.Ui/AccessibleNames.cs, shared by Lite and the Darling Viewer (no per-app code). Tests: Lite.Tests/ScreenReaderNamesFollowUpTests.cs and its twin Darling.Tests/ViewerScreenReaderNamesFollowUpTests.cs, 5 each, all on real DataGrids and a TabControl in a window.

  • Headers and tabs went stale when put back at the same size. Unloading a header or tab removed its text watcher and nothing looked again when it came back, because a header that returns at the same size gets no SizeChanged (the fixed-width Alert History "Time" and Job History "Run Time" columns). A screen reader kept reading "Time (server)" after the column was relabelled "Time (UTC)". Fix: Watch also hooks Loaded (remove, then add) with a handler that names the element from its current header, the way cells and rows already do. Test: a column header and a tab are taken out of the window, their text changes, and they are put back; both read the new text.
  • Row names did not follow column dragging. Rows are named from the first two visible columns in display order, but only re-read on activation, reuse or a text change. Fix: the first named row of a grid hooks DataGrid.ColumnDisplayIndexChanged once (a static handler on the grid's own event, so it ends with the grid); one drag raises it for several columns, so the re-read is coalesced and runs once at Loaded priority over the realized rows whose name is active. Test: moving the third column to the front renames the row from "Time 06:01:12, Server example-sql-01" to "Retries 3, Time 06:01:12".
  • Hidden elements in a templated cell counted as content. A collapsed text block with text made the cell non-blank, and a collapsed button made it content. Fix (2 lines): Collect skips any element that is not Visible. Its text is also left out of the row name. The full case, a template that swaps one element for another later (a trigger, a selector), is not followed: no CellTemplate in Lite or the Viewer does that today, so the cell is still judged again only when its text or content changes.
  • Heat-map and bar cells were never read. HeatIntensityToBrushConverter returns Brushes.Transparent for no heat, and Collect stopped at any panel or border with a non-null background, so every FinOps lock and latch cell was skipped; the bar cell is a UserControl, so the Executions, CPU, Duration and Reads bar columns were skipped too. Decision: read them, rather than skip them cleanly, because the value is the cell's main content and the row name should be able to use it. A brush that draws nothing (null, or alpha 0) counts as no background, so a transparent hit-test panel with no text is now blank; a visible background still counts as content, but the text inside it is collected. A ContentControl that is not a button (the bar cell) is descended through its visual children; content with no visual tree yet is never judged blank. Test: a transparent-border cell gives "Row Lock Wait ms 1,204" to the row name, an empty one is "Empty heat: blank", and a real BarChartCell gives "Executions 42".
  • Long parts in row names. Each part is now capped at 80 characters, ending in "..." (the Plan Corrections grid's Query Text column used to put the whole statement in the name). Test: a 5000-character query text gives a name of at most 100 characters, ending in "...".

RED. Before the fix, all 5 Lite follow-up tests failed (Total 5, Failed 5): header "Time (server)" still read after the change, row not renamed after the drag, hidden text cell not "Retries: blank", heat row name empty, 5000-character part not capped. The tab half of the first test was not reverted on its own; it uses the same Watch path as the header.

GREEN. Lite *ScreenReaderNamesFollowUpTests*, *ScreenReaderNamesTests*, *ScreenReaderRowNamesTests*, *ServerTabEmptyStateAndNamesTests*: Total 53, Failed 0. Darling.Tests *ViewerScreenReader*: Total 17, Failed 0. On the final head: Lite.Tests *Census*, *Ratchet*, *Hygiene*, *Scrub*, *Guard* Total 198, Failed 0; Darling.Tests the same five filters Total 939, Failed 0 (58 skipped, the live PostgreSQL tests, no test server set). Both test projects build with 0 warnings.

After the code check: FinOps, Alert History and Schedules

L5. The inventory dash blamed the CPU when the memory sample was missing. The Server Inventory health score is null when the last 24 hours hold no CPU sample, and also when the server has no memory row (memory_stats turned off in the schedule, or not yet run on a new server). All three surfaces said "No health score: the last 24 hours hold no CPU sample." Fix: one new constant per product, FinOpsHealthCalculator.NoInventoryScoreNote ("No health score: the last 24 hours hold no CPU sample, or no memory sample has been collected."), in Lite/Services/LocalDataService.FinOps.cs and Darling/PerformanceMonitor.Darling.Storage/FinOps/FinOpsScoring.cs. It is used where the inventory note is made: ServerPropertyRow.HealthScoreNote in Lite and in the Darling Viewer (the grid tooltip), and health_score_note in the MCP get_finops_inventory tool (DarlingMcpFinOpsInventoryTools.cs). The tool guide sentence now reads "with no CPU sample in the 24 hours, or no memory sample collected, there is no score (health_score_note says why)", and its guide pin is updated to match. The Utilization view keeps the old CPU-only note: it has no row at all without a memory sample, so the CPU reason is the only one there. The web Server Inventory page does not show the note (it is emitted but not shown, as on the desktop), so the web has nothing to change; its Utilization page shows the Utilization note, which stays correct.
Tests: FinOpsInventoryHealthScoreTests.NoScore_ShowsTheNoScoreColorAndNote (Lite), FinOpsInventoryViewerFiguresTests.NoScoreNote_NamesTheMissingMemorySampleAsWellAsTheCpuSample (Viewer), DarlingMcpFinOpsInventoryToolsTests.Row_HealthScoreAndBand_ComeFromTheSharedFigures (MCP) and the guide-head pin. RED with only the three note lines put back to the old constant: Lite FinOpsInventoryHealthScoreTests 1 of 2 failed (the L6 test below ran in the same run, 2 of 4 failed in all); Darling FinOpsInventoryViewerFiguresTests plus DarlingMcpFinOpsInventoryToolsTests 2 of 34 failed (the new Viewer test and the MCP no-CPU row). GREEN after.

L6. Lite's Alert History count ignored a column filter. After a column filter, Lite kept the count it showed before filtering, where the Viewer rebuilds it. Fix in Lite/Controls/AlertsHistoryTab.xaml.cs: the filter handler now calls a new ApplyColumnFilter, which applies the filter and rebuilds the count text with JobHistoryCap.CountText(AlertsDataGrid.Items.Count, _lastReadRowCount, RowCap, "alert(s)"), the same call the Viewer makes. The popup's Clear button raises the applied event with an empty filter, so clearing is covered by the same path (the separate cleared event only closes the popup). The load's tail moved into an internal ShowAlerts so a test can drive the tab. PerformanceMonitor.Ui/DataGridFilterManager.cs now falls back to a plain grey when the app has no resource dictionary (it threw a null reference there in a test with no running application; in the app nothing changes).
Test: AlertHistoryCountFilterTests (500 rows, seven match a server filter): "500 alert(s) (showing the newest 500)" becomes "7 alert(s) (showing the newest 500)" and returns to 500 when the filter is cleared. RED with the count line removed: 1 of 1 failed. GREEN after.

L7. Dead GetActivePresetForServer. Removed from Lite/Services/ScheduleManager.cs; nothing but its own tests called it. ServerScheduleSummaryTests no longer calls it: the default-schedule test keeps its check against GetActivePreset(), and the own-schedule test checks the summary against ScheduleManager.DetectPreset of that schedule. The test that pins the Settings window to the one summary read still asserts the window does not call the removed method.

Entry. The bold lead of the entry below now covers the whole change; every fact after it is kept, and the two changes above are added at its end. The squash text is patched the same way.

Test results (Lite and Darling.Tests build with 0 warnings; no PostgreSQL, so the 58 Darling live tests are skipped as in a run without a database):

  • Lite.Tests: AlertHistoryCountFilterTests, FinOpsInventoryHealthScoreTests, ServerScheduleSummaryTests, FinOpsIdleCoverageAndInventoryScoreTests plus every *Census*, *Ratchet*, *Hygiene*, *Scrub*, *Guard* class: Total 208, Failed 0.
  • Darling.Tests: FinOpsInventoryViewerFiguresTests, DarlingMcpFinOpsInventoryToolsTests, McpToolGuideHeads pins, FinOpsClickThroughRulesTests, the two FinOps page tests, plus the same wildcard set: Total 1179, Failed 0, Skipped 58 (live PostgreSQL). After merging the other part of this PR the Census/Ratchet/Hygiene/Scrub/Guard set plus the two touched classes ran again: Lite Total 208 Failed 0, Darling Total 979 Failed 0 Skipped 58.

CI checks fixed

Six checks failed on head f512dc2. All six were test-side; no product line changed. Fixed in a6b9cb8 (test files only). RED is the CI run on f512dc2; GREEN is the local run below.

  1. Paint sweep (ScopedLoadOrderingTests, 2 tests). Alert History's paint moved out of LoadAlertsAsync into ShowAlerts, so the sweep found no paint there. The supersession rule is still true: the ShowAlerts(alerts) call sits right below the Superseded check with no await between. The sweep now counts a ShowAlerts( call as a paint, so it sees the moved paint and a call placed above the check would still fail.
  2. Allocation check (AfterTheFirstLook_TheSizeChangedHandlers_DoNoAllocationAndAddNoWatchers, Viewer and Lite twin). CI saw 23,704 bytes over about 640,000 handler calls: a one-time cost, not a per-call allocation (a real per-call one would be megabytes). The measurement now warms up with 500 passes and takes the smallest of five 400-pass windows, so a one-off cost can land in at most one window while a real per-call allocation shows in every window. Checked that it still catches a real allocation: with GC.KeepAlive(new byte[16]) temporarily put in OnTabItemSized the test failed (224,000 bytes); restored, passes. The "no watchers added" half is unchanged.
  3. ViewerServerSetSyncTests.EverySurfaceThatListsServers_ReadsTheFleetTheReloadRebuilds. The Overview total now goes through OverviewCardView.BuildRollup(cards, totals, list.Count, ...). The pin now requires list.Count as that call's registered count and _overviewRegisteredCount = list.Count (the value a search-box change rebuilds the roll-up with), so the total still comes from the fleet the reload rebuilt.
  4. DarlingMcpJobHistoryToolLiveTests.ViewerRows_EqualTheStorageReaderRows_MappedThroughFrom. The product is right: each Viewer job row is stamped with its own server's clock (the per-row clock change), and the storage reader's row carries none. The test mapped the storage rows without a clock. ServerClock serializes as {} whatever it holds, so comparing JSON could not check it. The test now asserts every Viewer row has a clock equal (by offset) to the clock the fleet's collected offsets give that row's server, then compares the rest of the row through From with that clock. Ran on a test PostgreSQL server: passes.
  5. ConsumedTimestampFrameDisciplineTests.TheOneHopRenderWrappers_AreExactlyTheDeclaredSet. FormatLastCollect (Last Collect date change) is a legitimate one-hop wrapper: it hands the stored naive-UTC collection time to FormatForDisplay with the display zone and only picks the format. Added to the declared set with that reason.

GREEN on the final head (after merging origin/dev), 0 build warnings in Darling.Tests and Lite.Tests:

  • Darling.Tests (*Census*, *Ratchet*, *Hygiene*, *Scrub*, *Guard*, plus the five classes above): Total 1002, Failed 0, Skipped 60 (live tests with no database set). With a test PostgreSQL server set, before that merge: Total 1026, Failed 0, Skipped 0, including the live job-history classes.
  • Lite.Tests (same wildcards plus ScreenReaderRowNamesTests): Total 215, Failed 0.

Not done: nothing left from the six.

SECTION: Fixed
ENTRY:

  • Small fixes across Lite and the Darling Viewer from the final release walk: Collector Schedules wording, Server Inventory health, time columns, screen-reader names and FinOps dashes ([Release checks: small fixes from the final walk of Lite and the Darling Viewer #5542]) - Every server showed Preset "Custom" next to Status "Default", which read as a contradiction. Status now reads "Uses default" or "Own schedule", both column headers have a tooltip, and a Custom preset has a tooltip naming the collector intervals that differ from the closest preset. The Server Inventory Health column in Lite and the Darling Viewer, web page and MCP tool now shows the same score as that server's Utilization view; it used to show almost the same number for every server because it scored average CPU against fixed memory and storage values. The Darling Viewer's Overview card shows the date of a collection that is not from today, the Alert History and Job History time columns name the clock they read on (and Viewer Job History rows read on their own server's clock), Alert History says when it shows only the newest 500, the Overview search also narrows the fleet roll-up above the cards ("Monitoring 2 of 9 servers"), and an Availability Group on old data reads Stale instead of Healthy in the Viewer and Lite. Screen readers now read column headers, their filter buttons, empty cells (text or templated), grid rows and server list items by name in Lite and the Viewer (they read as "System.Windows.Controls.StackPanel", a bare glyph or a type name before; a row now reads as its first two visible columns, such as "Time 06:01:12, Server example-sql-01"). The FinOps Utilization view in Lite and the Darling Viewer shows dashes for Avg, P95 and Max CPU when a server has no CPU sample in the last 24 hours (they read 0.00%, 0.00% and 0%) and hides the database sizes chart then, and the web page's Utilization read and the MCP tool return null for those three figures. The Server Inventory dash in place of a health score now has a tooltip that names both reasons for it (no CPU sample in the last 24 hours, or no memory sample collected), and Lite's Alert History count follows a column filter, as the Darling Viewer's does.

Re-cut v3.8.0: Velopack upload hardening (#3521) to main
CI: the full-tier review runs up to 120 turns (dev to main)
…lock, Alert History notes its cap, Overview search narrows the roll-up, a stale Availability Group reads Stale
…ver list items in Lite and the Darling Viewer
…des the stale database sizes chart; Storage Growth baselines pinned
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review October 8, 2026 07:11
…d blank templated cells, and the name handlers stop re-reading after the first look
…llow column order, hidden and heat/bar cells read right, long parts capped
…rt History count follows a column filter, dead GetActivePresetForServer removed
…Alerts, allocation test uses the smallest window, pins follow the new shapes
@erikdarlingdata
erikdarlingdata merged commit b5bc69a into dev Oct 8, 2026
25 of 26 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/release-walk-1008 branch October 8, 2026 09:28
erikdarlingdata added a commit that referenced this pull request Oct 8, 2026
Adds the changelog entries for the 3.10.0 PRs that merged after the first splice: #5528, #5532, #5537, #5542, #5543, #5552 and #5553. Each full entry goes into docs/changelog/3.10.md and its title-only line into the CHANGELOG.md index. The archive census row for 3.10.0 moves from 257 to 265 entries.

Changelog entries for the 3.10.0 PRs merged after the first splice.
@erikdarlingdata erikdarlingdata mentioned this pull request Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant