Skip to content

Availability Groups tab in the WPF viewer (#991) - #1722

Merged
erikdarlingdata merged 2 commits into
devfrom
feature/991-ag-viewer-tab
Jul 26, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
feature/991-ag-viewer-tab

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

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 ThemeCompletenessTests stays 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_states returns 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 DarlingAgReader is internal to it, so the reader is copied rather than referenced — the established idiom here. The duplication is deliberate and pinned on both sides: ViewerAvailabilityGroupsTests restates 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 collect through search_path, and a test asserts no collect. prefix leaks in), and both reads join the enabled server registry so a disabled server's AG cards leave with it.

Ratchet retired

ViewerCollectorCoverageTests.KnownStoreOnlyOrUnbuiltTables listed ag_replica_states and ag_database_replica_states as "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 the ViewerDataServiceTests and FleetViewTests shapes. 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's darling-pg job 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

erikdarlingdata and others added 2 commits July 26, 2026 18:06
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
erikdarlingdata merged commit 0e2c370 into dev Jul 26, 2026
4 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/991-ag-viewer-tab branch July 26, 2026 22:23
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>
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