Repository navigation
Availability Groups tab in the WPF viewer (#991) - #1722
Merged
Merged
Conversation
The AG topology on the client's primary surface, completing the reader half of #991. Desktop twin of the web dashboard's Availability Groups page: one card per AG with per-replica chips (role, connected/operational state, synchronization health, availability + failover mode, endpoint on hover) over a database grid carrying synchronization state, queue sizes, rates, secondary lag, derived drain estimates, and the suspend reason. Every severity is computed in the reader; the XAML only binds the brush it produced, so the tab cannot drift from the web page's verdict. Brushes are the viewer's existing frozen severity palette -- no new theme keys, so the three theme files stay in sync. ONE CARD PER (reporting server, AG), deliberately not merged. The live AG fixture shows why: the primary reports both replicas and names the primary, while the secondary reports ONE replica and no primary at all, because sys.dm_hadr_availability_replica_states returns only local information when queried on a secondary. Collapsing the two views would let the blind one overwrite the complete one. The header therefore states groups, reporting servers and views as three separate counts instead of leaving a reader to wonder why one AG name appears twice. The tab ships Collapsed and reveals itself once a sweep finds AG rows -- Always On is opt-in and most fleets would otherwise carry a permanently empty tab. It converges without a restart, and the reveal is one-way within a session so a collector hiccup cannot make the tab vanish under the operator mid-look. An AG-less fleet pays one indexed read that returns nothing, because the database-grain read is skipped when there are no replicas. The reader is a COPY of the service's DarlingAgReader, not a reference: the viewer has no ProjectReference to the headless service. The duplication is deliberate and pinned by tests on both sides. The viewer's copy reads bare table names (search_path resolves collect) and joins the enabled registry so a disabled server's AG cards leave with it. Retires the last two entries in the viewer-coverage ratchet. ag_replica_states and ag_database_replica_states were carried as tracked debt when #991 shipped collection-only; the pin that tracked them is the same pin that now certifies the surface exists, and it only ever shrinks. Lite gets no AG tab in this pass: it is a single-server app and the fleet view is Darling's job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
erikdarlingdata
enabled auto-merge
July 26, 2026 22:07
pull Bot
pushed a commit
to ehtick/PerformanceMonitor
that referenced
this pull request
Jul 29, 2026
…rlingdata#991) Lite gets the AG topology tab Darling's viewer got in erikdarlingdata#1722, reading its own local DuckDB, and the banding and card-shaping rules move into PerformanceMonitor.Common.AgTopology so the web dashboard, the Darling viewer and Lite all draw the same AG from one set of rules. There were two copies of the projection after erikdarlingdata#1722; there is now one. Twin test matrices in both test projects fail together if the shared rules change, which is what makes "these surfaces cannot disagree" a checked claim rather than an intention. Brushes stay per app because Common cannot reference WPF: the model carries the verdict, a SeverityBrushConverter carries the colour. That also removed the brush properties from the models entirely, which is a better split than the one erikdarlingdata#1722 shipped. Lite needed its OWN read, and that is the part worth knowing. The AG alert read from erikdarlingdata#1696 looks reusable and is not: it selects five columns where a topology view needs twelve, and it DROPS rows whose ag_name or replica_server_name is NULL. That is correct for alerting -- those are the state-key identity, and a row that cannot be keyed cannot have an edge tracked for it -- but a topology view must do the opposite, because those NULLs appear under WSFC quorum loss, which is exactly when an operator opens the page. Lite's read also correlates its latest-snapshot MAX per server rather than globally, so a lagging instance is not erased by a livelier one's newer timestamp. Same hidden-until-AG-rows reveal as Darling's, same honest empty state, same worst-first ordering. Pointed at a secondary, Lite shows one replica and no primary -- that is what that server can actually see, and saying so beats implying it knows the whole group. Stacked on erikdarlingdata#1696 (erikdarlingdata#1726) for Lite's AG collector registration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Completes the reader half of #991 on the surface the client actually uses. Release-blocking per Erik: the WPF viewer is the primary surface.
What it is
The desktop twin of the web dashboard's Availability Groups page. One card per AG: header with the AG name, a worst-health badge and the current primary; replica chips carrying role, connected/operational state, synchronization health, availability + failover mode, and the endpoint on hover; then a database grid with synchronization state, log-send and redo queue sizes (KB), send/redo rates (KB/s), secondary lag, derived drain estimates, and the suspend reason when data movement has stopped.
Every severity is computed in the reader and the XAML binds only the brush it produced, so the tab cannot reach a different verdict than the web page. Brushes are the viewer's existing frozen severity palette — no new theme keys, so
ThemeCompletenessTestsstays green without touching all three theme files.The design point worth reviewing
One card per (reporting server, AG), deliberately not merged — the same rule the web page follows, and the live AG fixture demonstrates why. Queried on the primary, the DMV reports both replicas and names the primary. Queried on the secondary, it reports one replica and no primary at all, because
sys.dm_hadr_availability_replica_statesreturns only local information from a secondary. Collapsing those two perspectives — the obvious "one AG, one card" instinct — lets the blind view overwrite the complete one.So the same AG legitimately renders twice, and the header says so out loud with three separate counts (
1 group · 2 reporting servers · 2 views) rather than leaving a reader to wonder why a name repeats.Hidden until it has something to show
The TabItem ships
Visibility="Collapsed"and reveals itself once a sweep finds AG rows. Always On is opt-in; most fleets would otherwise carry a permanently empty tab. The probe runs on the existing refresh timer only while hidden, so an AG fleet does not pay for it twice, and it converges without a restart when someone stands an AG up. The reveal is one-way within a session — a collector hiccup reading zero must not make a tab vanish under the operator mid-look. An AG-less fleet costs one indexed read returning nothing, because the database-grain read is skipped when there are no replicas.The reader is a copy, on purpose
The viewer has no ProjectReference to the headless service and
DarlingAgReaderis internal to it, so the reader is copied rather than referenced — the established idiom here. The duplication is deliberate and pinned on both sides:ViewerAvailabilityGroupsTestsrestates the banding expectations so the two cannot silently drift into disagreeing about whether an AG is healthy.Two adaptations: the viewer reads bare table names (its connection resolves
collectthroughsearch_path, and a test asserts nocollect.prefix leaks in), and both reads join the enabled server registry so a disabled server's AG cards leave with it.Ratchet retired
ViewerCollectorCoverageTests.KnownStoreOnlyOrUnbuiltTableslistedag_replica_statesandag_database_replica_statesas "UNBUILT UI — remove when the tab ships". Both are deleted here. That pin fails the moment a viewer reader touches a listed table, so the pin that tracked this as debt is the same pin that certifies it done — and it only ever shrinks, so a future collector shipping without a viewer surface still gets caught.Tests
SQL pins (dialect, bare-table, latest-snapshot-not-
DISTINCT ON, enabled-registry join) plus banding theories and pure card-projection tests, mirroring theViewerDataServiceTestsandFleetViewTestsshapes. The projection tests cover the load-bearing cases: two servers reporting one AG stay two cards with the secondary showing no primary; Unknown never masks Healthy in the roll-up; a suspended database reddens a card whose replicas all read healthy; databases attach only to their own reporting server's card; worst-first ordering.Darling.Tests: 3280 passed, 0 failed (ungated). The gated-live half is skipped here because my local Postgres rig was contested by a parallel agent mid-run; CI'sdarling-pgjob provisions its own fresh cluster and covers it. Viewer builds with 0 warnings.Deliberately not done
Lite gets no AG tab in this pass — it is a single-server app and the fleet view is Darling's. Noted so it reads as a decision rather than an oversight.
🤖 Generated with Claude Code