Skip to content

Band Lite's collection freshness, so a quiet server stops looking green (#2452) - #2457

Merged
erikdarlingdata merged 2 commits into
devfrom
fix/2452-lite-collection-freshness
Aug 21, 2026
Merged

Band Lite's collection freshness, so a quiet server stops looking green (#2452)#2457
erikdarlingdata merged 2 commits into
devfrom
fix/2452-lite-collection-freshness

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Fixes #2452 (part 1). Part 2 is split out as #2458 — see the last section for why, and why the issue's reason for pairing them did not survive the implementation.

The defect, and why it is reachable in shipped Lite

The Overview card carries five metric rows and a status word. None of them was collection freshness, so the state the issue describes — connection fine, nothing erroring, store taking no new rows for hours — rendered as a green "Online", a neutral border, and a timestamp in the plain foreground brush where four hours ago looked exactly like four seconds ago.

The issue asks for a measurement: whether Lite installs actually see collection stop without a collector erroring. Lite's own source answers it without needing one, and the path is short:

  • RemoteCollectorService.RecordCollectorResult treats PERMISSIONS as not a failure — deliberately, per its comment, because a permission denial is not transient. It sets IsPermissionRestricted and leaves ConsecutiveErrors alone.
  • IsCollectorPermissionRestricted then returns out of RunCollectorAsync before LogCollectionAsync, so a restricted collector writes no collection_log row at all — not even a failed one.
  • GetHealthSummary counts ErroringCollectors from ConsecutiveErrors > 0.

So a login that loses rights across the board stops the store dead with ErroringCollectors at zero, the connection check still passing, and every card green. _collectorHealth is in-memory too, so an app restart zeroes every streak while the store stays exactly as stale as the previous session left it.

What changed, and the one thing that deliberately did not

Option 1 from the issue, sharing the thresholds rather than growing new ones.

ServerHealthClassifier.ClassifyFreshness out of PerformanceMonitor.Common — the same Fresh / Stale / Offline / NeverCollected ladder the Darling viewer bands, off the same ServerHealthThresholds numbers. Lite already referenced that project, so this cost one using. No threshold is added to Lite, and that is a claim about correctness rather than convenience: Lite's fastest configured collector in collection_schedule.json runs every minute, which is exactly the derivation CollectorCadence documents, so StaleThreshold is true here on its own terms rather than borrowed from a service with a different rhythm.

Option 2 — folding freshness into the status word — is turned down, and the reason is written on StatusDisplay. Lite's status word is a connection word (IsOnline is a live check, which succeeds whether or not anything is being collected) and its "Warning" already means one specific thing: collectors are erroring. #2429 found the viewer renders that same word for a stale collection and for a metric breach with nothing on the card telling them apart, which is the card @ehaar wrote in about in #2422. Reproducing that here would have traded one defect for the other.

So freshness bands its own row and names its own band there. What the card actually renders, printed from the shipped ServerSummaryItem spliced out of this branch:

collection status word border Last Collect row card tooltip's middle line
current Online green neutral #2A2D35 11:59:40 green Every metric on this card is inside its threshold
stale (2m01s) Online green amber #FFD54F 11:57:59 (stale) amber Needs attention: Last collect 11:57:59 (stale)
stopped (4h) Online green amber #FFD54F 08:00:00 (stopped) red Needs attention: Last collect 08:00:00 (stopped)
never collected Online green amber #FFD54F Never amber Needs attention: Last collect Never

and the Last Collect row carries its own tooltip, which is the part that answers the "do not reproduce the conflation" requirement literally:

Collection is stale — nothing has landed for over 2 minutes.
This is about collection stopping, not about the server's metrics: the connection check can still be passing and no collector reporting an error.

The status word staying green across all four rows is not an oversight — it is the point. IsOnline answers "did the last connection check succeed", and it did. "Online" and "fresh" are different questions and the card now answers both, each in its own place, each in its own words. The thresholds inside the tooltip are read from ServerHealthThresholds rather than typed, so the sentence cannot come to disagree with the band it is explaining.

Three smaller decisions

The border escalates, on its own arm reading the same amber as the collector-error one. Collector errors and collector silence are one 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; a fix that left the card looking calm at a glance would have fixed a row nobody was asked to read. It stays amber even for a stopped collection: red belongs to the offline overlay, and a red border under a green "Online" would be the loudest contradiction on the card. The row underneath carries the severity in its own colour and its own word.

It is a separate arm rather than a clause on the collector-error one because #2451's TheCardBorder_RendersTheSameDiscriminantTheWordDoes pins that arm's source text — CardStatus == ServerCardStatus.CollectorErrors ? "#FFD54F" — and folding || CollectionIsNotFresh in put the new clause between the discriminant and the colour, so the literal stopped appearing. That pin is a landed guard from another lane asserting something true and worth asserting, so the branch moved rather than the pin. Two named causes reading one colour is the better shape anyway: each arm gets its own line and its own reason.

This was caught by running #2451's committed test class against this branch rather than waiting for CI to run it — and CI then failed on that one test and nothing else, which is the whole argument for doing it that way. All 12 of #2451's tests pass on this branch now.

NeverCollected is amber, never red. A server that has not been collected yet is queued, not dead — the reasoning ServerFreshness.NeverCollected already carries from the viewer, where a red "Offline" on a merely-queued server sent a 24-server field report chasing a phantom scheduler bug.

The band is stamped in GetServerSummaryAsync, the one place a ServerSummaryItem is built, rather than at the Overview loader — the two MCP reads call that method too, and a band only the Overview applied would be a fact that depended on which caller asked. The clock is handed in rather than read inside the band, so the classification stays pure over (last collection, now). null means nobody classified it: unreachable in the app, reachable from a fixture, and it renders exactly what dev rendered — the bare stamp, the card's unknown grey, no tooltip.

Background="Transparent" on both row cells is load-bearing rather than decoration, 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 and nowhere in the space around them.

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 ServerSummaryItem straight out of the shipped file (a generator reads the source and emits it — never retyped) behind a System.Windows.Media shim, with a real project reference to PerformanceMonitor.Common so the bands are the shipped classifier's. The table above is that harness's output.

dev this branch
the full committed test file does not compile — 29 errors 16 passed / 0 failed
the identically-expressible subset 3 passed / 5 failed 8 passed / 0 failed

The absence is the finding rather than a measurement, so the subset runs identical bodies against both contracts. ApplyCollectionFreshness is invoked reflectively there — on dev there is genuinely nothing to stamp, which is the thing being measured, not a harness limit. The five that fail on dev are the quiet server's border, the row naming its band, the card tooltip naming the row, the XAML wiring, and the stamp itself. The three that pass on both sides are the controls, and they are the ones a wrong fix breaks first: a calm currently-collecting card keeps the neutral border (so this is not "paint every card amber"), the status word says the same thing for a stale card as for a fresh one (so this is not option 2 sneaking in), and a calm current card's tooltip still says nothing about collection.

EveryCardIsStampedWithItsBandWhereItIsBuilt is a source scan on purpose: deleting that one line compiles perfectly clean and silently returns every card to dev's behaviour, and a property nothing sets is this issue's own failure mode one level up. The XAML half has to text-scan for the same reason #2429 gave — a ToolTip attribute lives where no assertion about a C# object can reach it, and removing it compiles clean.

dev moved twice while this was open (#2451, then #2450/#2453). Rebased onto 88fe948f and re-verified: whole solution builds 0 errors, and 28 cases green — this PR's 16 plus all 12 of #2451's, run against the rebased tree. Not force-pushed, because the PR is MERGEABLE/CLEAN against that same dev and the rebase was for verification rather than for the merge.

Whole solution builds, 0 errors. CHANGELOG deliberately untouched — #2395 is an open release-prep PR that owns that file for 3.5.1.

Part 2 is split out as #2458, not dropped

#2452's second half — ServerConnection.DotStatus being a fourth independent copy of the four-word ladder — is filed as #2458 rather than done here, and the issue's own reason for pairing them does not hold as this landed. It argued that "a freshness band would otherwise need writing twice". It is written once, on ServerSummaryItem, and the sidebar dot is untouched, so nothing is duplicated by leaving it.

The thing that does argue for splitting: the type DotStatus should collapse onto is ServerCardStatus, and in Lite that type arrives in #2451, which is still open. Doing it here would mean either duplicating that enum or stacking on an unmerged branch — and a stacked PR reports a meaningless green.

#2451 landed while this was open, and it is what completes option 1

The issue's option 1 has two halves: band the row, and "let the tooltip name it, the way it names CPU and Blocking today". The second half needed a tooltip built from the card's own rows, which did not exist in Lite when #2452 was written. #2451 merged into dev mid-flight and brought one, so this branch was rebuilt on top of it rather than textually merged, and takes the whole option instead of half.

CollectionIsNotFresh joins CpuIsElevated / BlockingIsElevated / DeadlocksAreElevated in #2451's per-row concern gates, and StatusReason quotes LastCollectionDisplay verbatim exactly as it quotes CpuDisplay — which is the property that makes that whole design work, per its own comment: the clause and the row cannot render different things. Because the display already carries the band word, the clause names its axis 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.

TheTooltipNamesTheCollectionRowExactlyWhenItIsNotGreen extends #2451's own invariant to this row and asserts it against the shipped brush rather than a copy of the thresholds:

Assert.Equal(
    Hex(card.LastCollectionBrush) != Green,
    card.StatusTooltip.Contains("Last collect ", StringComparison.Ordinal));

🤖 Generated with Claude Code

…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>
@erikdarlingdata
erikdarlingdata force-pushed the fix/2452-lite-collection-freshness branch from bae028e to 10f6840 Compare August 21, 2026 17:25
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed the diff (Lite/Services/LocalDataService.Overview.cs, Lite/MainWindow.xaml, Lite.Tests/ServerCollectionFreshnessTests.cs). No blocking issues found.

Correctness

  • ApplyCollectionFreshness is stamped once inside GetServerSummaryAsync, which is confirmed to be the single construction site for ServerSummaryItem — both Lite/Mcp/McpHealthTools.cs and Lite/Mcp/McpDiscoveryTools.cs call the same method, so the "one place, two MCP reads" claim in the PR description checks out.
  • The freshness classification is independent of IsOnline/HasCollectorErrors, which are set later by the caller in MainWindow.xaml.cs:723 — no ordering hazard between the two.
  • Boundary/null handling (unclassified null band → grey/no tooltip, NeverCollected → amber not red, strict > on thresholds) matches the shared ServerHealthClassifier in PerformanceMonitor.Common, which this PR doesn't modify.
  • CollectionFreshnessTooltip/LastCollectionDisplay read thresholds from ServerHealthThresholds rather than restating literals, so the tooltip text can't drift from the band boundaries.

Lite/Darling parity

Security/Performance

  • No new SQL, no new I/O, no new external input handling. One extra DateTime.UtcNow + enum classification per card per refresh — negligible.

Nice level of test coverage pinning the anti-conflation invariant (status word never reflects freshness) and the XAML wiring via source-scan tests, given nothing here is otherwise reachable by a C# assertion.

…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>
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed. This is a well-scoped, well-tested change and I don't see any correctness, security, or parity issues to block on.

What it does: Stamps ServerSummaryItem.CollectionFreshness (via the shared ServerHealthClassifier.ClassifyFreshness in PerformanceMonitor.Common) at the single construction site in LocalDataService.GetServerSummaryAsync, so the Overview card's "Last Collect" row gets its own band/brush/tooltip instead of the plain foreground brush it had before. All three callers (MainWindow.RefreshOverviewAsync, McpHealthTools.GetServerSummary, McpDiscoveryTools) funnel through that one method, and a test pins the single construction + stamp-after-construction ordering in source.

Correctness checks I ran through:

  • ClassifyFreshness's > (strict) comparisons make the boundary values (StaleThreshold, OfflineThreshold) land in the lower band, matching the new test's TheBoundariesAreTheSharedClassifiersAndAreInclusiveOfTheLowerBand and matching the classifier's existing (untouched) behavior used by Darling.
  • CollectorCadence = 1 minute is verified against Lite/config/collection_schedule.json — several collectors (wait_stats, cpu_utilization, memory_stats, etc.) really do run at frequency_minutes: 1, so the PR's claim that no new threshold is needed for Lite holds up.
  • Both instants passed into the classifier are UTC (DateTime.UtcNow and the DuckDB-stored naive-UTC timestamp), so the subtraction is a true elapsed time regardless of Kind, as documented.
  • The status word (StatusDisplay/StatusBrush) deliberately stays untouched by freshness — confirmed CardStatus only reads (IsOnline, HasCollectorErrors), so the "Warning" word still means exactly one thing. This is the right call given Lite's status word is a live-connection check, unlike Darling's headless service which has no live ping and therefore derives status from freshness by necessity (DarlingMcpDataTools.FreshnessStatus) — so this isn't Lite/Darling drift, it's the two apps correctly doing different things for different reasons, and it's the same conflation Say why the card is amber, and give "+52 more" somewhere to go (#2424) #2429/[QUESTION] Overview Warnings #2422 already fixed once.
  • No other code path reads LastCollectionDisplay/LastCollectionBrush/CollectionFreshnessTooltip, so appending " (stale)"/" (stopped)" to the display string can't break another consumer.
  • XAML: both new TextBlocks get Background="Transparent" so the tooltip hit-tests over the whole cell rather than just the glyphs — matches the #2429 note and is tested (TheCardsLastCollectRowIsBoundToTheBand).

No T-SQL in this diff, so the collector-query style conventions don't apply here. I wasn't able to actually run dotnet build/dotnet test in this sandbox (commands require interactive approval I don't have), so this is a static review only — worth having CI confirm the new Lite.Tests/ServerCollectionFreshnessTests.cs suite passes before merge.

@erikdarlingdata
erikdarlingdata merged commit 23335b7 into dev Aug 21, 2026
8 checks passed
erikdarlingdata added a commit that referenced this pull request Aug 21, 2026
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>
MisterZeus pushed a commit to MisterZeus/PerformanceMonitor that referenced this pull request Aug 24, 2026
…ered differently

On dev, a registered-but-never-collected server draws a grey "Unknown" dot in the sidebar
and an amber "Awaiting first collection" card one panel over. Both are stamped from the
SAME ClassifyFreshness call. Verified before touching anything, and the premise holds
exactly as erikdarlingdata#2473 describes it: DarlingServer.ApplyFreshness sets two flags where
ServerSummaryItem.ApplyFreshness sets three, DarlingServer has no AwaitingFirstCollection
to set, and MainWindow.xaml carries DataTriggers for Online/Offline/Warning only -- so the
fifth state fell through to ForegroundMutedBrush and painted grey without failing anything.

The dot is the thing a reader points at first, and it was silently giving the pre-erikdarlingdata#2429
answer.

WHAT THE DOT NOW SAYS, AND WHY

Amber, matching the card, with the card's word. The card already chose amber for this state
and wrote down why -- a queued server is not a dead one, which is the 24-server field
incident erikdarlingdata#1552 answered. Grey is arguably honest ("nothing is known yet") and it was the
smaller change, but it is a SECOND answer to a question the card has already answered on
the same screen, and having two answers is the entire defect. The sidebar's job here is to
render the card's decision, not to hold an opinion about it.

Amber now covers two states on the dot (a stale collection and a never-collected server),
which is exactly what the card already does -- the card disambiguates them with a word and
a dot has no room for one, so the dot gains a tooltip instead. Its first line is the shared
headline, word for word what the card says.

WHERE THE LADDER LIVES

Not in the viewer. Collapsing the two viewer surfaces onto a viewer-local rules class would
have produced a pin that could not span the files holding the other copies, and erikdarlingdata#2470's
whole lesson is that a pin scoped shorter than the duplication is worth nothing. Looking
for those files turned up two more:

  - DarlingFleetReader.StatusLabel -- the web/MCP fleet card's word. Agrees with the card
    today; a fourth independent derivation of the same five states.
  - DarlingMcpDataTools.FreshnessStatus -- list_servers, written as if statements, in
    another file, against its OWN copies of the 2-minute and 15-minute thresholds. That is
    the erikdarlingdata#2470 evasion shape twice over, plus a real drift bug: ServerHealthThresholds could
    move and list_servers would go on answering with the old numbers.

Both live in the headless service, which cannot reference WPF, so the only assembly all
four can render from is PerformanceMonitor.Common -- where ServerHealthClassifier and the
thresholds already are, and where ClassifyBand already takes this exact flag triple.

The type is ServerCollectionStatus, not ServerCardStatus, and the rename is load-bearing
rather than cosmetic. Lite has a ServerCardStatus of its own answering a DIFFERENT question
-- its word comes from a live connection check, this one from how old the newest collection
is -- and erikdarlingdata#2457 turned down folding freshness into Lite's word precisely to keep the two
axes apart. Common already names that distinction (ServerConnectionStatus is the connection
answer); this is the collection one. Sharing a name would have invited the conflation both
issues were about, and Lite/Models/ServerConnection.cs imports both namespaces, so it would
also not have compiled.

WHAT THE COLLAPSE ACTUALLY IS

Classify(isOnline, hasCollectorErrors, awaitingFirstCollection) is the ladder. Word(),
McpToken() and Headline() are three renderings of its result -- not three ladders, which is
the correction erikdarlingdata#2470 had to make once already: a second switch is a second ladder even when
it returns the right type. McpToken differs from Word in exactly one arm because
"AwaitingFirstCollection" shipped as a published MCP status value and clients key on it; a
status value is a consumer API, so the vocabularies stay apart and sit next to each other
where the difference reads as a decision.

FlagsFor(ServerFreshness) is the other half, and it is the half that fixes the bug rather
than tidying it. Both ApplyFreshness implementations classified freshness and then wrote
the flags out by hand; the sidebar's wrote two of three and dropped the awaiting marker on
the floor. Nothing about a block of assignments makes a missing one visible. They now
arrive as one value.

Behaviour is unchanged everywhere except the sidebar dot. The MCP list_servers tokens were
checked arm by arm against the thresholds they used to duplicate (2 min stale, 15 min
offline -- identical), and StatusLabel's output is byte-identical.

Verified on macOS: Common, the Darling viewer, the Darling service, Lite, Lite.Tests and
Darling.Tests all build. The ladder itself was run against the real build in a throwaway
net10.0 console -- every freshness band through FlagsFor and Classify agrees with
FromFreshness, the erikdarlingdata#2429 flag pairs still resolve the way the card always resolved them,
and all four list_servers tokens match the pre-change thresholds. The WPF suites cannot run
on macOS; CI is the arbiter for those.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata deleted the fix/2452-lite-collection-freshness branch September 12, 2026 20:29
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