Say why Lite's card is amber, and which axis the word is about (#2437) - #2451
Conversation
Lite/MainWindow.xaml:442 was the same bare Text="{Binding StatusDisplay}"
with no ToolTip that #2429 fixed in the Darling viewer. A reader saw a word in
a colour and had to scan the metric rows guessing which one the card meant, on
every card. That is @ehaar's #2422 question -- "what is it that this text warns
me about?" -- on a second surface.
This is a port rather than a wiring change, because Lite has no FleetRollup and
no BuildReason, so the reason had to be built. The property worth copying
carefully is the one that makes the viewer's version work at all: BuildReason
is assembled from the card's OWN metric displays, so it cannot drift from the
rows underneath it. A Lite implementation that recomputed severity would
satisfy every wording assertion and still be worse than no tooltip, because the
one place it would disagree is a card the reader is staring at.
So StatusReason is built from CpuDisplay, BlockingDisplay and DeadlockDisplay
verbatim, and gated on the SAME predicates the row brushes are painted from --
which meant giving each row's "not green" test a name and having both readers
use it instead of carrying their own copy of the threshold. The test asserts
that against the shipped brushes rather than against a copy of the numbers: for
every fixture, the tooltip names CPU exactly when CpuBrush is not the healthy
green, and likewise for Blocking and Deadlocks. Memory is deliberately absent,
because the Memory row carries no severity brush -- there is no band on this
card to report, and inventing one is the single thing this property exists to
prevent.
Lite's tooltip has one more thing to say than the viewer's, and this is the
part that is not a copy. The viewer's status word is a health BAND; Lite's is a
CONNECTION word. Its "Warning" means ErroringCollectors > 0 -- collectors that
are genuinely failing -- and says nothing about the metric rows, while a card
at 96% CPU with two deadlocks still reads a green "Online" over a red border.
So Lite does NOT have the viewer's stale-collection conflation (Lite's IsOnline
comes from a live connection check, not from freshness), it has the inverse
one, and fusing the two axes into a single clause would reproduce the ambiguity
in prose. The tooltip keeps them on separate lines: what the status word means,
then what the rows say.
An Offline card gets the first line and no metric line. The card draws a
dimming overlay across those rows precisely because the numbers under it are
the last ones collected before the server went dark, and demanding attention
for them would contradict the card while the reader is looking at it.
StatusReason still computes them -- the omission is a tooltip decision made in
one place, not a hole in what the card knows.
The (IsOnline, HasCollectorErrors) pair now has one reader. The word and the
colour already carried their own copy of that ladder and the tooltip would have
been a third; ServerCardStatus is the viewer's collapse ported, and its amber
member is named CollectorErrors rather than Stale because that is what it
actually means here.
Background="Transparent" on the TextBlock is load-bearing rather than
decorative, as #2429 found: a TextBlock with a null Background hit-tests on its
rendered glyphs alone, so the tooltip would have appeared over the letters of
"Warning" and nowhere in the space around them. The status dot carries the same
tooltip, since it is the same signal and the thing a reader points at first.
Lite has no AwaitingFirstCollection flag, which I checked before relying on it,
so the status/tooltip desync family the viewer spent four review rounds on
cannot arise here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review summaryScope: C#/XAML only (Lite's Overview card tooltip), no T-SQL touched, so the T-SQL style/parity checks in CONTRIBUTING.md don't apply here directly. Correctness. Traced the new Lite/Darling parity. This is intentionally a Lite-only catch-up on work the Darling viewer already got in #2429/#2422, and the PR body is explicit and accurate about where Lite's semantics diverge (connection-word status vs. the viewer's stale-collection conflation, no Security / performance. No I/O, network, process, or user-input handling introduced — pure computed UI properties over data that was already being read. Left one non-blocking inline nit on Otherwise this is a clean, well-tested port — nothing blocking. |
Review caught this and it is right, and it is worse than the doc-accuracy nit it was raised as. ServerCardStatus's comment claimed the (IsOnline, HasCollectorErrors) pair was read in one place; CardBorderBrush read IsOffline and HasCollectorErrors raw, so it agreed with the status word by coincidence rather than by construction. It already disagreed on one pair. A card that has never been connection-checked but carries a collector-error marker draws the amber "collectors failing" border -- verified against dev's own copy of the class, which returns #FFD54F there -- while its word reads "Unknown" and its tooltip says the server has not been checked yet. That is precisely the desync family this change exists to close, sitting in the one member it had not reached. The Overview loader only sets HasCollectorErrors when the connection check succeeded, so the pair is unreached in practice. That is the argument for making it unrepresentable rather than for leaving it alone: both flags are plain settable properties, and "no caller does this today" is what the viewer's two review rounds on #2429 each disproved. So the border reads CardStatus, and so does IsOffline, which the offline overlay binds. Precedence is unchanged -- a dark server, then the metric rows worst-first, then failing collectors -- and the amber border is now literally the same amber the status word is painted, because both render one state. The claim in the doc comment is now true, and a test asserts it rather than leaving it as prose for the next reviewer to re-check by hand: exactly one `IsOnline switch` in the file. That is the assertion that would have caught the overclaim when it was written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed the diff (Lite.Tests/LiteOverviewCardExplainsItselfTests.cs, Lite/MainWindow.xaml, Lite/Services/LocalDataService.Overview.cs). No blocking issues found. Correctness — traced the
Lite/Darling parity — verified against Security / perf — no user input, network, file, or process handling introduced; property getters are cheap and only reoccur on WPF binding refresh (unchanged pattern from before this PR). Nice touch that the second commit is itself a self-caught fix ( |
…ng green Lite's Overview card carried five metric rows and a status word and none of them was collection freshness. The failure mode that leaves is not exotic: the connection check keeps passing, no collector is *erroring*, and the store takes no new rows for hours. IsOnline comes from ServerManager.GetConnectionStatus -- a live check that succeeds whether or not anything is being collected -- and HasCollectorErrors counts collectors with ConsecutiveErrors > 0, which a collector that has stopped being scheduled never increments. So the card showed a green "Online", a neutral border, and a "Last Collect" timestamp painted in the plain foreground brush, where four hours ago looked exactly like four seconds ago. Lite reaches that state through its own shipped code. A collector answering PERMISSIONS sets IsPermissionRestricted and RunCollectorAsync returns BEFORE LogCollectionAsync, so it writes no collection_log row at all -- and the PERMISSIONS arm of RecordCollectorResult deliberately does not touch ConsecutiveErrors, because a permission denial is not a transient failure. A login that loses rights across the board therefore stops the store dead with ErroringCollectors at zero. The health map is in-memory as well, so a restart zeroes every streak while the store stays as stale as the old session left it. This takes option 1 from the issue, in full. The band is ServerHealthClassifier.ClassifyFreshness from PerformanceMonitor.Common -- the same Fresh / Stale / Offline / NeverCollected ladder the Darling viewer bands, off the same ServerHealthThresholds numbers, and Lite already referenced that project. No threshold is added to Lite: its fastest configured collector runs every minute, which is the derivation CollectorCadence documents, so the shared constant is true here on its own terms rather than borrowed. Option 2 -- folding freshness into the status word -- is turned down, and the reason is written on CardStatus so the next reader has the argument in front of them. Lite's status word is a CONNECTION word and CollectorErrors already means one specific thing. Folding freshness in would make one amber word mean two unrelated failures, which is the conflation #2429 spent four review rounds untangling on the viewer and the reason #2422 was written. So freshness bands its own row and names its own band there: "08:00:00 (stopped)" in red, with a tooltip that says in words that this is about collection stopping rather than about the server's metrics. #2451 landed while this was open and it is what makes the rest of option 1 possible: the issue asked for the tooltip to "name it, the way it names CPU and Blocking today", and there is now a StatusReason built out of the card's own row displays to name it in. CollectionIsNotFresh joins the per-row concern gates beside CpuIsElevated and BlockingIsElevated, and the clause quotes LastCollectionDisplay verbatim like every other part there -- so the sentence and the row cannot render different things, and because the display already carries the band word the clause says which axis it is about without borrowing a metric's vocabulary. It goes FIRST in the reason. Collection is the row that says whether the other four can be believed at all: a card whose collection stopped four hours ago is showing four-hour-old CPU, and meeting "CPU 4%" before learning that is the wrong order to be told the two facts in. The card border escalates too, sharing the amber arm with CollectorErrors, because collector errors and collector silence are the same class of fault -- the monitoring of this server is not working -- and silence is the harder of the two to notice, which is the whole issue. It stays amber even for a stopped collection: red belongs to a dark server, and a red border under a green "Online" would be the loudest contradiction on the card. The band is stamped in GetServerSummaryAsync, the one place a ServerSummaryItem is built, so the two MCP reads get the same answer the Overview does rather than a fact that depends on which caller asked. Null means nobody classified it -- unreachable in the app, reachable from a fixture -- and renders exactly what the row rendered before: the bare stamp, the card's unknown grey, no tooltip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ctor one Caught by running #2451's committed test class against this branch rather than waiting for CI to run it, and CI then failed on exactly this one test and nothing else. TheCardBorder_RendersTheSameDiscriminantTheWordDoes pins the border's source text: Assert.Contains("CardStatus == ServerCardStatus.CollectorErrors ? \"#FFD54F\"", source) Folding freshness in as "CardStatus == ServerCardStatus.CollectorErrors || CollectionIsNotFresh ? ..." put the new clause between the discriminant and the colour, so the literal stopped appearing. The pin is a landed guard from another lane and it is asserting something true and worth asserting -- that the border reads the same state the word does, which review on #2451 found it had not been doing -- so the branch moves, not the pin. Two named causes reading one colour is the better shape anyway: each arm gets its own line and its own reason, and "the monitoring of this server is not working" stays one idea expressed as two conditions rather than one condition with a comment explaining that it is really two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ServerConnection.DotStatus computed "Unknown"/"Online"/"Warning"/"Offline" from its own (IsOnline, HasCollectorErrors) pair -- the same four-word ladder the Overview card derives, on a different type, on a different surface, from a different instance of the same flags. The two could say different things about one server and nothing would notice. That is the drift #2429 collapsed the viewer's card for and #2451 collapsed Lite's, and this is the last of them in Lite. Not the last of them anywhere, and an earlier draft of this message said so. Review caught it: DarlingServer.DotStatus in the Darling viewer is a fifth copy, and unlike this one it is not merely capable of disagreeing with its card -- on dev it already does. Both surfaces there classify the SAME ServerFreshness, and a never-collected server draws a grey "Unknown" dot beside an amber "Awaiting first collection" card. That is a different app, a different enum and a different vocabulary decision, so it is filed rather than folded in here. The collapse is not "ServerConnection switches on the pair too, but returns an enum". A second switch is a second ladder however it is typed, so the ladder itself moved into one function: ServerCardStatusRules.Classify, with Word() and Headline() beside it. ServerSummaryItem.CardStatus and ServerConnection.CardStatus both call it, and StatusDisplay/DotStatus and StatusHeadline/DotTooltip are renderings of the result. The card's brushes stay where they are -- hex colours are the card's business and the sidebar paints from theme resources -- but they still switch on CardStatus, not on the flags. The dot gets the tooltip the card's dot got in #2451, opening on the same sentence word for word. It is the thing a reader points at first, which is #2422's complaint one surface over from where it was reported. Its middle line is the part this surface specifically needs. #2457 deliberately kept collection freshness OUT of the status word and gave it its own banded row on the card; the sidebar has no such row and ServerConnection carries no last-collection time to build one from, so a green dot here is a connection answer read somewhere that offers no freshness answer at all. The tooltip says so and names where the freshness answer lives, rather than leaving a reader to infer one from the other -- which is the #2429/#2422 conflation in miniature. Classify takes two arguments and a test names that signature, so folding freshness in cannot happen by accident. #2451's pin moved with the ladder rather than being loosened, and it got wider, because the way it missed this copy is instructive. Assert.Equal(1, CountOccurrences(source, "IsOnline switch")) counted one literal in one file. The fourth copy was in another file AND was written as a chain of if statements rather than a switch, so it evaded that pin on both axes at once and went on evading it through #2451 and #2457. The count now spans both files and pins the classifier's own parameter list; and a new assertion holds the invariant the count was standing in for, syntax-agnostically -- the four words are string literals that appear in exactly one function, and none of them is written in ServerConnection.cs any more. Verification. Lite.Tests targets net10.0-windows and cannot run on macOS, so the logic was run for real anyway: a throwaway net10.0 harness splices ServerCardStatus, ServerCardStatusRules, ServerSummaryItem and the ServerConnection members straight out of the shipped files (a generator reads the source and emits them -- never retyped) behind a System.Windows.Media shim, with a real project reference to PerformanceMonitor.Common. All 35 checks pass, including every reachable (IsOnline, HasCollectorErrors) combination agreeing between the two surfaces. Against dev the strongly-typed harness does not compile at all, so the comparison is run as the identically-expressible subset with the new members reached reflectively: 7 passed / 6 failed on dev, 13 / 0 on the branch. The 7 that pass on both are the controls, and they are the ones a wrong fix breaks first -- the two ladders agree TODAY, which is why this defect is drift potential rather than a live disagreement, and why the assertions that separate the branches are about construction rather than output. The source- and XAML-scanning half was simulated in Python against both versions: 13 fail on dev and all 18 pass on the branch. Whole solution builds, 0 errors. CHANGELOG deliberately untouched -- #2395 is an open release-prep PR that owns that file for 3.5.1. One thing noted and not fixed here: Lite/Mcp/McpDiscoveryTools.cs renders a THIRD vocabulary for the same axis -- "Online"/"Offline"/"Status not checked" -- off ConnectionStatus.IsOnline. It is a different type, it never reads collector health so it has no "Warning" state, and the MCP text is a consumer API, so changing its words is a decision rather than a cleanup. Filed as a separate concern rather than folded in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Part 2 of #2437 — the Lite half. (The web dashboard half is #2447: different language, different risk.)
Lite/MainWindow.xaml:442was the same bare binding #2429 fixed in the Darling viewer:A word, a colour, and no way to ask what it meant. A Lite user reads "Warning" and scans the metric rows for the yellow one, exactly as @ehaar had to on the viewer in #2422.
The reason is built from the card's own rows, not recomputed
This is a port, not a wiring change: Lite has no
FleetRollupand noBuildReason, so the reason had to be built. The property worth copying carefully is the one that makes the viewer's version work at all —BuildReasonis assembled from the card's OWN metric displays, so it cannot drift from the six rows underneath it. A Lite implementation that recomputed severity would satisfy every wording assertion in the test file and still be worse than no tooltip, because the one place it would disagree is a card the reader is staring at.StatusReasontherefore quotesCpuDisplay,BlockingDisplayandDeadlockDisplayverbatim, and is gated on the same predicates the row brushes are painted from. That meant giving each row's "not green" test a name and having both readers use it rather than each carrying its own copy of the threshold:and the test asserts the invariant against the shipped brushes, not against a copy of the numbers:
Memory is deliberately absent. The Memory row carries no severity brush on this card, so there is no band to report — inventing a threshold there is the single thing this property exists to prevent.
Lite's conflation is not the viewer's, and the tooltip has to say so
Checked in source rather than assumed. The viewer derives its status from collection freshness and re-purposes
HasCollectorErrorsto mean "stale" — its own doc comment says so. Lite'sIsOnlinecomes from_serverManager.GetConnectionStatus(...), a live connection check, and itsHasCollectorErrorsfrom_collectorService.GetHealthSummary(server).ErroringCollectors > 0, which counts collectors withConsecutiveErrors > 0. So Lite does not have the stale-collection conflation — its amber "Warning" is a real collector-failure state.It has the inverse one instead, and it is arguably sharper. Lite's status word is a connection word, not a health band: it never reflects the metric rows at all, so a card at 96% CPU with two deadlocks reads a green "Online" while its border is red. Fusing the two axes into one clause would reproduce that ambiguity in prose, so the tooltip keeps them on separate lines — what the status word means, then what the rows say.
What the tooltips actually read, run against the shipped source:
Online — the last connection check succeeded/Every metric on this card is inside its thresholdOnline — the last connection check succeeded/Needs attention: CPU 96%, Deadlocks 2Warning — one or more collectors are failing on this server/Every metric on this card is inside its thresholdWarning — one or more collectors are failing on this server/Needs attention: CPU 62%Offline — the last connection check failedUnknown — this server has not been connection-checked yet/Needs attention: CPU 62%each closing on
Double-click the card to open this server's tab— the viewer's line verbatim, pinned rather than left to coincidence, because the point of doing all three surfaces is that a reader moving between them meets one vocabulary.Row 2 is the one to look at. That card is in real trouble and the only thing on it that ever said so was a border colour.
Three smaller decisions
An Offline card gets no metric line. The card draws a dimming overlay across those rows precisely because the numbers under it are the last ones collected before the server went dark; demanding attention for them would contradict the card while the reader is looking at it.
StatusReasonstill computes them — the omission is a tooltip decision made in one place, not a hole in what the card knows.The
(IsOnline, HasCollectorErrors)pair now has one reader. The word and the colour already carried their own copy of that ladder and the tooltip would have been a third.ServerCardStatusis #2429's collapse ported; its amber member is namedCollectorErrorsrather thanStalebecause that is what it means here, and the difference is written down where the next person will find it.Review found a fifth reading I had missed, and it turned out to be a live desync rather than a latent one.
CardBorderBrushreadIsOfflineandHasCollectorErrorsraw, so ondeva card that has never been connection-checked but carries a collector-error marker draws the amber "collectors failing" border while its word readsUnknown:The border and
IsOffline(which the offline overlay binds) now renderCardStatustoo; precedence is unchanged, and the amber border is literally the same amber the word is painted because both render one state. The doc claim that the flag pair has one reader is now asserted rather than left as prose —Assert.Equal(1, CountOccurrences(source, "IsOnline switch"))— which is the assertion that would have caught the overclaim when it was written.Background="Transparent"is load-bearing, as #2429 found: aTextBlockwith a nullBackgroundhit-tests on its rendered glyphs alone, so the tooltip would have appeared over the letters of "Warning" and nowhere in the space around them. The status dot carries the same tooltip — same signal, and it is the thing a reader points at first.Lite has no
AwaitingFirstCollectionflag — verified before relying on it — so the status/tooltip desync family #2429 spent four review rounds on cannot arise here. Only the unexplained-word half applied.Verification
Lite.Teststargetsnet10.0-windowsand cannot run on macOS, so the logic was run for real anyway: a throwawaynet10.0harness splicesServerCardStatusandServerSummaryItemstraight out of the shipped file (never retyped — a generator reads the source and emits it) behind aSystem.Windows.Mediashim, and all 12 tests in the new class pass against it. The table above is that harness's output. Pointed atdev's copy of the same file it does not compile at all, because none of the members exist there.The source- and XAML-scanning half was simulated in Python against both versions: 20 of 23 assertions fail on
devand all pass on the branch. The three that pass ondevare the two guards that locate the status TextBlock and the status dot, plus the viewer's half of the closing-line parity pin, which this PR does not touch. That half has to text-scan, because aToolTipattribute lives in XAML where no assertion about a C# object can reach it and removing it compiles perfectly clean.Whole solution builds, 0 errors. CHANGELOG deliberately untouched — #2395 is an open release-prep PR that owns that file for 3.5.1.
One thing noted and not fixed here:
ServerConnection.DotStatus(Lite/Models/ServerConnection.cs:199) is a fourth copy of the same four-word ladder, for the sidebar row. It is a different type on a different surface and unifying it is not this PR's lane, but it is the next place this can drift.🤖 Generated with Claude Code