Repository navigation
Release checks: small fixes from the final walk of Lite and the Darling Viewer - #5542
Merged
Merged
Conversation
Release 3.4.0
Release 3.5.0
Release 3.5.0
Release 3.6.0
Release v3.7.0
Release v3.8.0
Re-cut v3.8.0: Velopack upload hardening (#3521) to main
Release 3.9.0
CI: the full-tier review runs up to 120 turns (dev to main)
…tree-agent-a3149c7f2a84eb476
…schedule reads Custom
…he Utilization tab's score
…tree-agent-a3149c7f2a84eb476
…llow the shared score
…lock, Alert History notes its cap, Overview search narrows the roll-up, a stale Availability Group reads Stale
…release-walk-1008
…ver list items in Lite and the Darling Viewer
…des the stale database sizes chart; Storage Growth baselines pinned
…release-walk-1008-a11y
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
…release-walk-1008
…llow column order, hidden and heat/bar cells read right, long parts capped
…rt History count follows a column filter, dead GetActivePresetForServer removed
…tree-agent-afe51b75c1dc60967
…Alerts, allocation test uses the smallest window, pins follow the new shapes
8 of 9 tasks
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.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
deadlocksat 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:deadlocks1 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 exampledeadlocks 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 checkedDarling/PerformanceMonitor.Darling.Viewer/CollectorScheduleEditorWindow.xamlandCollectorSchedulePresets.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 olddeadlocksinterval 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_inventorytool and the web Server Inventory page had the same defect throughFinOpsInventoryFigures.HealthScore, so they got the same fix.Fix. One rule,
FinOpsHealthCalculator.Scorein Lite (Lite/Services/LocalDataService.FinOps.cs) andFinOpsUtilizationFigures.HealthScorein Darling. The fleet read behind the Inventory grid (GetServerMetricsAsyncin Lite,DarlingFinOpsInventoryReader.ServerMetricsSqlin 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-basedInventoryScoreandFinOpsInventoryFigures.HealthScoreare 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'sget_finops_inventorydescription 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, andDarlingMcpFinOpsInventoryTools.cs.Tests. Lite:
FinOpsFleetReadParityTests.ServerMetrics_HealthScore_EqualsTheUtilizationScore_PerServerseeds 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,AzureSqlDatabaseOwnFiguresTestsin both projects,ViewerFinOpsInventoryDelegatesTests,FinOpsInventoryViewerFiguresTests,FinOpsClickThroughRulesTests, the inventory tool guide-head pin) now pin the new call. The inventory golden fixture gains theHealthScorefield, 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
FinOpsFleetReadParityTestsTotal 7, Failed 1; DarlingFinOpsInventoryHealthScoreLiveTestsTotal 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 withDocCommentHygieneTests);*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.LastCollectionDisplayalways formattedHH: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) usesHH:mm:sswhen the wall time is today in the display zone andyyyy-MM-dd HH:mm:ssotherwise. Lite: checkedLocalDataService.Overview.cs, it already does this. I changedViewerW2aTests.LastCollectionDisplay_TreatsStoredValueAsUtc_ShownLocalto 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 inGetJobHistoryAsyncfrom 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.BuildRolluprebuilds the roll-up from the cards the search leaves; the subtitle reads "Monitoring 2 of 9 servers" while filtered (FleetRollup.FilteredOfTotal).MainWindow.ApplyOverviewCardFilterre-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 (searchedLite/MainWindow.xaml*).D10. The Availability Groups tab showed a green "Healthy" badge on 13-day-old data. Fix:
AgTopology.BuildCardstakes the clock and marks a card whose newest replica snapshot is older thanServerHealthThresholds.OfflineThreshold(the 30 minutes the web page's stale rule uses) asIsStale: label "Stale", band at least Warning (never Unknown, which sorts below Healthy). The stale flag is in the refresh digest and copied byUpdateFrom, 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 failedTsqlConventionGuardTests.TheMemberScan_ReadsEveryDeclarationWholeon myMonitoringText(is { }pattern); I rewrote it withHasValueand 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
AccessibleNamesclass already named tabs and column headers, but it hookedLoadedwith a class handler. WPF raisesLoadedstraight 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 "". HookingSizeChanged(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'sToString(), 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 onSizeChanged(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 onServerConnection(with the "(Read-Only)" suffix the box shows), Lite'sServerSummaryItem, and the Viewer'sDarlingServer,ServerSummaryItemandFleetRankedServer. Lite's Recommendations box and the Viewer's FinOps and Recommendations server boxes getAutomationProperties.Name="Server"and an item container style naming each item. The Viewer sharesAccessibleNames(itsApp.xaml.csalready 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.csandDarling.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) getsAvgCpuText,P95CpuTextandMaxCpuText(a dash whenHasCpuSampleis false, the figure otherwise, so a measured idle 0 still reads 0) andShowsDatabaseSizeChart(false with no CPU sample). The Viewer'sFinOpsTab.Loaders.csand Lite'sFinOpsTab.xaml.cspaint the three texts from the row, leave the three CPU bars empty with no sample, and collapse the sizes chart group (namedFinOpsDbSizeChartGroup/DbSizeChartGroupin 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 theutilizationMCP read, which now returnsnull(not 0) forcpu.avg_cpu_pct,p95_cpu_pctandmax_cpu_pctwhen 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, whichViewerDataService.GetStorageGrowthAsynccalls) 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: newFinOpsUtilizationNoCpuTests(6, the Lite twin) and two tests inFinOpsStorageGrowthBaselineTests(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
utilizationMCP 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;FinOpsStorageGrowthBaselineTestsTotal 8 Failed 0;StorageGrowthNoHistoryLiveTestsTotal 4 Failed 0;FinOpsUtilizationViewLiveTestsTotal 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;FinOpsTabUtilizationPageTests10/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 atTextBlockcells. Separately, the shared handlers ran on everySizeChanged; 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).DataGridRownames 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.Activeflag. 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
ToStringprinting properties; each returns only the display name (LiteServerConnectionreturns 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:ServerConnection, LiteServerSummaryItem, ViewerDarlingServer,ServerSummaryItem,FleetRankedServer, interpolation and+: every site found formats.DisplayName,.Idor.ServerNameexplicitly (the{server}hits inEntraDeviceCodeAuth,RemoteCollectorService.LogWatermarkReadFailureandAppare strings) - unchanged meaning.{Server}placeholders): the arguments areserver.DisplayName;LogEnumerationProbeFailures(definition, server, ...)takes the object and logsserver.DisplayName- unchanged meaning.string.Join: onlyDescribeServeras an explicit selector in the Viewer's main window - unchanged meaning.Convert.ToString,.Tag/.Content/.SelectedItem...ToString(): the hits readComboBoxItemtags and content, which are strings - unchanged meaning.Equals/GetHashCodeare not overridden, so identity is unchanged.ToString.{Binding}header templates bind strings; the other lists useDisplayMemberPath.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.csandDarling.Tests/ViewerScreenReaderRowNamesTests.cs, 5 each, all on realDataGrids in a window: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,
ScreenReaderRowNamesTestsTotal 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.csand its twinDarling.Tests/ViewerScreenReaderNamesFollowUpTests.cs, 5 each, all on realDataGrids and aTabControlin a window.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:Watchalso hooksLoaded(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.DataGrid.ColumnDisplayIndexChangedonce (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".Collectskips any element that is notVisible. 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: noCellTemplatein Lite or the Viewer does that today, so the cell is still judged again only when its text or content changes.HeatIntensityToBrushConverterreturnsBrushes.Transparentfor no heat, andCollectstopped at any panel or border with a non-null background, so every FinOps lock and latch cell was skipped; the bar cell is aUserControl, 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. AContentControlthat 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 realBarChartCellgives "Executions 42".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
Watchpath 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."), inLite/Services/LocalDataService.FinOps.csandDarling/PerformanceMonitor.Darling.Storage/FinOps/FinOpsScoring.cs. It is used where the inventory note is made:ServerPropertyRow.HealthScoreNotein Lite and in the Darling Viewer (the grid tooltip), andhealth_score_notein the MCPget_finops_inventorytool (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: LiteFinOpsInventoryHealthScoreTests1 of 2 failed (the L6 test below ran in the same run, 2 of 4 failed in all); DarlingFinOpsInventoryViewerFiguresTestsplusDarlingMcpFinOpsInventoryToolsTests2 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 newApplyColumnFilter, which applies the filter and rebuilds the count text withJobHistoryCap.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 internalShowAlertsso a test can drive the tab.PerformanceMonitor.Ui/DataGridFilterManager.csnow 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 fromLite/Services/ScheduleManager.cs; nothing but its own tests called it.ServerScheduleSummaryTestsno longer calls it: the default-schedule test keeps its check againstGetActivePreset(), and the own-schedule test checks the summary againstScheduleManager.DetectPresetof 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):
AlertHistoryCountFilterTests,FinOpsInventoryHealthScoreTests,ServerScheduleSummaryTests,FinOpsIdleCoverageAndInventoryScoreTestsplus every*Census*,*Ratchet*,*Hygiene*,*Scrub*,*Guard*class: Total 208, Failed 0.FinOpsInventoryViewerFiguresTests,DarlingMcpFinOpsInventoryToolsTests,McpToolGuideHeadspins,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.
ScopedLoadOrderingTests, 2 tests). Alert History's paint moved out ofLoadAlertsAsyncintoShowAlerts, so the sweep found no paint there. The supersession rule is still true: theShowAlerts(alerts)call sits right below theSupersededcheck with no await between. The sweep now counts aShowAlerts(call as a paint, so it sees the moved paint and a call placed above the check would still fail.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: withGC.KeepAlive(new byte[16])temporarily put inOnTabItemSizedthe test failed (224,000 bytes); restored, passes. The "no watchers added" half is unchanged.ViewerServerSetSyncTests.EverySurfaceThatListsServers_ReadsTheFleetTheReloadRebuilds. The Overview total now goes throughOverviewCardView.BuildRollup(cards, totals, list.Count, ...). The pin now requireslist.Countas 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.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.ServerClockserializes 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 throughFromwith that clock. Ran on a test PostgreSQL server: passes.ConsumedTimestampFrameDisciplineTests.TheOneHopRenderWrappers_AreExactlyTheDeclaredSet.FormatLastCollect(Last Collect date change) is a legitimate one-hop wrapper: it hands the stored naive-UTC collection time toFormatForDisplaywith 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:*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.ScreenReaderRowNamesTests): Total 215, Failed 0.Not done: nothing left from the six.
SECTION: Fixed
ENTRY: