Skip to content

Give the web dashboard's "+52 more" somewhere to go (#2437) - #2447

Merged
erikdarlingdata merged 6 commits into
devfrom
fix/2437-web-attention-filter
Aug 21, 2026
Merged

Give the web dashboard's "+52 more" somewhere to go (#2437)#2447
erikdarlingdata merged 6 commits into
devfrom
fix/2437-web-attention-filter

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Part 1 of #2437 — the web dashboard half. (The Lite half is a separate PR: different language, different risk.)

fleet.js:168 rendered the overflow as an inert muted div:

nodes.push(el("div", { class: "muted", style: "margin:0.4rem 0 0.2rem",
                       text: "+ " + d.additional_problem_count + " more need attention" }));

A number, a colour, and nothing to click. @ehaar asked the same question of the desktop twin in #2422"where do I find these warnings?" — and the honest answer on this surface was the same: nowhere. Those 52 servers are not missing data. BuildRollup computes the whole problem set and then truncates it to DefaultWorstCount for display, so everything past the fifth is discarded rather than unavailable.

The line is now the way to them

Activating it turns on a needs-attention filter over the card grid immediately below — the surface that actually answers the question, because it is where the six metric chips live. #2429 settled two properties on the viewer and both port intact.

The filter runs the same predicate the count was computed from

BuildRollup counts cards.Where(c => c.Band != FleetHealthBand.Healthy); the payload already carries that band per card, banded server-side by the shared ServerHealthClassifier; cardNeedsAttention reads it off the card. So the destination cannot disagree with the label that sent you there, and R1 still holds — no threshold is re-derived in the browser. A client-side approximation of "needs attention" is not a shortcut here, it is this defect wearing a new hat.

That is pinned against both artifacts rather than transcribed:

var healthyToken = JsonSerializer.Serialize(FleetHealthBand.Healthy, DarlingFleetReader.JsonOptions);
Assert.Equal("\"Healthy\"", healthyToken);
Assert.Contains("return c.band !== " + healthyToken + ";", FleetJs, StringComparison.Ordinal);

Renaming the band member now breaks the test instead of silently emptying the grid the link lands on.

An active filter is visible and clearable

The viewer can put its count beside the toggle and stop there, because its roll-up header is docked and never scrolls. This page head scrolls away, so the notice rides with the cards instead and carries its own way out (Show all servers). Its wording is the desktop's, word for word — showing 12 of 57, and the all-clear arms too, because a filtered grid holding nothing is otherwise an empty page with no explanation.

The colour follows the sentence, not the filter. That is a review finding from #2429 ported rather than rediscovered: this line says either a count of servers wanting attention or an all-clear, and painting an all-clear amber would be a colour contradicting its own text — the family of defect this whole change is about.

state notice colour
12 of 57 need attention Needs attention only — showing 12 of 57. amber
...with off also typed in Search Needs attention only, matching “off” — showing 2 of 2. amber
fleet is calm, filter on Needs attention only — all 57 servers are healthy. green
single-server install Needs attention only — the 1 server monitored is healthy. green
search matched nothing Needs attention only, matching “xyz” — nothing matches that term, so no server was judged. neutral
search narrowed it, all calm Needs attention only, matching “warn0” — all 10 matching servers are healthy. green

Rows 2 and 5 are follow-up commits, both of them this PR's own defect turned on itself. The first version counted against the fleet, which is fine alone and wrong once a search term is also narrowing the grid — showing 4 of 57 invites reading 4 as the fleet's problem count. And a term that matched nothing left shown at 0, so it took the green all-clear arm: the colour that means everything is fine, over a fleet whose problem servers were never looked at. That is the same colour-contradicting-its-own-text finding #2429's review raised about the amber arm, ported without noticing it had a third case.

Row 6 is the third: AttentionFilterCountText is ported from a surface where it is unconditionally true, because the viewer's Overview has no search box and its total is the fleet. Here total is what the search left, so the 1 server monitored is healthy claimed the fleet held one server while 56 others existed and were never looked at. The unnarrowed arms stay verbatim — the cross-surface vocabulary pin still holds and still means something — and with a term active the sentence names the search instead.

The notice also carries role="status", matching util.js's noticeStrip — it appears and re-words itself with no page load, and a filter you can reach by keyboard whose result is never announced is a half-finished job.

The toggle sits beside Search and Sort because it is the same kind of thing — a view control over the same cards — and it is where the state lives: the link turns it on rather than filtering behind its back, so either affordance can undo the other. It is deliberately not persisted the way the sort and the grouped view are (both are in localStorage): a sort is a preference, this is a triage action tied to a moment, and a page that opens with 52 of 57 servers already hidden is a support ticket even with the box in plain sight.

onActivate rather than onClick, because that is util.el's path that also installs role="button", a tabindex and Enter/Space — the same treatment every other clickable div on this page already gets; a plain click handler would look identical and be invisible to a keyboard.

One sentence per state, including the empty ones. The notice explains an empty grid whenever it is showing, so the grid-area fallback is suppressed under it rather than stacking a second box that says the same thing in different words — the desktop viewer shows only its count and leaves the grid blank, and this now matches. What remains is the case the notice does not cover: the filter off, where the search term is the only thing that can have emptied the grid.

Verification

This repository has no JavaScript test runner, so the pins text-scan the shipped module, the way ViewerGridPayloadColumnOrderPinTests already scans server.js. Six of the seven fail against dev; the one that passes is the guard that finds the overflow line at all plus the server-side arithmetic, which has not changed.

Behaviour was verified for real anyway, because a source scan cannot see whether the thing works. The shipped fleet.js was run under a minimal DOM shim (no npm, no jsdom — createElement/createTextNode and a stubbed fetch) against a fabricated /api/fleet body of the shape BuildRollup emits: 31 assertions, all passing, over a 57-server fleet, a mixed fleet, an all-healthy fleet and a one-server install — including that the filtered grid holds exactly the 12 problem servers and no healthy card, that the five already ranked are still reachable in it, that search composes with the filter, and the keyboard path in and out. Against dev the same harness cannot get past the third assertion, because the affordance does not exist.

One thing noted and not fixed here, raised in review and pre-existing: renderGrouped's own empty-state branch is unreachable whenever lastTags.length > 0, because buildTagGroups emits a header per tag in the forest even when no card falls under it. So narrowing to zero in the grouped view renders a tree of zero-count headers rather than a message. That was already true of the search box; the attention filter is a second way to reach it. It is cosmetic rather than confusing here — the notice sits above the tree and says what happened — and unpicking it means changing how the grouped view treats empty groups, which is #2020's lane rather than this one.

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

🤖 Generated with Claude Code

The fleet page rendered the overflow as an inert muted div -- a number, a
colour, and nothing to click. @ehaar asked the same question of the desktop
twin in #2422 ("where do I find these warnings?") and the honest answer here
was the same: nowhere. The 52 servers are not missing data. BuildRollup
computes the whole problem set and then truncates it to DefaultWorstCount for
display, so everything past the fifth is discarded rather than unavailable.

The line is now the way to them. Activating it turns on a needs-attention
filter over the card grid immediately below, which is the surface that actually
answers the question because it is where the six metric chips live. #2429
settled two properties on the viewer and both port intact.

The first is that the filter runs the SAME predicate the count was computed
from. BuildRollup counts `Band != Healthy`, the payload carries that band per
card already banded by the shared classifier, and cardNeedsAttention reads it
off the card -- so the destination cannot disagree with the label that sent you
there, and R1 still holds because no threshold is re-derived in the browser. A
client-side approximation of "needs attention" is not a shortcut here, it is
this defect wearing a new hat. The test pins that against both artifacts: it
runs the reduction's own arithmetic, and it builds the literal the browser
compares against out of the shipped serializer rather than typing it out, so
renaming the band breaks the test instead of silently emptying the grid.

The second is that an active filter is visible and clearable. The viewer can
put its count beside the toggle and stop, because its roll-up header is docked
and never scrolls; this page head scrolls away, so the notice rides with the
cards instead and carries its own way out. It says what the desktop says, word
for word -- "showing 12 of 57", and the all-clear arms too, since a filtered
grid holding nothing is otherwise an empty page with no explanation. The colour
follows the sentence rather than the filter, which is a review finding from
#2429: painting an all-clear amber would be a colour contradicting its own
text.

The toggle sits beside Search and Sort because it is the same kind of thing, a
view control over the same cards, and it is where the state lives -- the link
turns it on rather than filtering behind its back. It is deliberately not
persisted the way the sort and the grouped view are: a sort is a preference,
this is a triage action tied to a moment, and a page that opens with 52 of 57
servers already hidden is a support ticket even with the box in plain sight.

The empty-grid line had to learn which filter emptied it. Reusing the search
wording unguarded would tell a reader whose fleet is simply healthy that
nothing matches a term they never typed.

This repository has no JavaScript test runner, so the pins text-scan the
shipped module the way ViewerGridPayloadColumnOrderPinTests already scans
server.js. Six of the seven fail against dev. Behaviour was verified for real
by running that same file under a minimal DOM shim against a fabricated
/api/fleet body: 23 assertions across a 57-server fleet, a mixed fleet, an
all-healthy fleet and a one-server install, including the keyboard path in and
out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed. This is Darling-only (web dashboard) work with no T-SQL touched, so the collector/style conventions don't apply here, and the Lite half is explicitly out of scope for this PR (separate PR per the description) — no parity gap to flag.

Checked and confirmed solid:

  • cardNeedsAttention reads the server-computed band field (c.band !== "Healthy") rather than re-deriving a threshold client-side — matches DarlingFleetReader.BuildRollup's Band != FleetHealthBand.Healthy predicate, and the "band" JSON property name lines up with FleetHealthBand's [JsonPropertyName("band")].
  • Wording (showing {shown} of {total}, all {total} servers are healthy, the 1 server monitored is healthy, no servers to filter) is byte-for-byte identical to ViewerDataService.Fleet.cs's desktop viewer strings — verified directly in the source, not just taken on the PR description's word.
  • util.el's onActivate is used (not onClick) for both the "+N more" line and "Show all servers", so the affordance gets role="button", a tabindex, and Enter/Space — consistent with this page's other clickable divs, and keyboard-reachable.
  • All rendered text goes through el()'s text:/textContent path — no html prop, no XSS surface introduced.
  • mount(gridNode, [notice, renderGrouped(matched)]) is safe with notice === nullappendChildren in util.js skips null/false and recurses into nested arrays, so both the flat and grouped render paths correctly show/hide the notice.
  • attentionOnly module state persists across the 60s poll (renderFleet rebuilds pageHead/the checkbox each refresh and re-syncs cb.checked = attentionOnly), and is deliberately not persisted to localStorage unlike sort/grouped-view state — reasonable call given it's described as a triage action rather than a durable preference.
  • Filter composes with search (cardMatches(c, fleetFilter) && (!attentionOnly || cardNeedsAttention(c))) at a single call site, and the empty-grid message correctly distinguishes "no search match" vs "no servers need attention" vs "no search match while filtered."

No correctness, security, or performance issues found. Nice test coverage pinning the JS module content against the shared C# artifacts (enum serialization, viewer wording) so a rename breaks the test instead of silently drifting.

My own notice had the defect this PR is about. It read "showing N of 57" with
the fleet as the denominator, which is fine on its own and wrong the moment a
search term is also narrowing the grid: "showing 4 of 57" invites reading 4 as
the fleet's problem count, and the other 53 were not judged healthy, they were
never looked at. The label has to mean what the grid holds.

The two filters now compose explicitly. The search runs first, its result is
the denominator, and the notice names the term when there is one -- "Needs
attention only, matching "off" -- showing 2 of 2." With no term the wording is
unchanged, because searched.length is then the whole fleet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@erikdarlingdata

Copy link
Copy Markdown
Owner Author

Follow-up commit: my own notice had the defect this PR is about.

The count read showing N of 57 with the fleet as the denominator. That is fine on its own and wrong the moment a search term is also narrowing the grid — showing 4 of 57 invites reading 4 as the fleet's problem count, and the other 53 were not judged healthy, they were never looked at.

The two filters now compose explicitly: the search runs first, its result is the denominator, and the notice names the term when there is one — Needs attention only, matching “off” — showing 2 of 2. With no term, searched.length is the whole fleet and the wording is unchanged.

Harness is now 24 assertions, all passing against the branch; the C# pins still split 6 of 7 against dev.

@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed the diff (fleet.js, app.css, and the new FleetPageAttentionFilterTests.cs — no T-SQL in this PR).

What I checked:

  • cardNeedsAttention reads c.band !== "Healthy" off the pre-banded payload field, matching the server-side predicate BuildRollup uses (cards.Where(c => c.Band != FleetHealthBand.Healthy)) — no client-side threshold re-derivation (R1 holds).
  • Wording parity: attentionCountText/attentionNotice text is verbatim-identical to ViewerDataService.Fleet.AttentionFilterCountText, confirmed against the current viewer source, not just the PR's claim.
  • mount()/appendChildren() handle the null notice element fine when the filter is off (checked util.js).
  • attentionToggle/gridNode module-state lifecycle across the 60s poll (app.js refresh()route() → full renderFleet()): no stale-node risk since pageHead() and gridNode are both freshly recreated each full render, and redrawCards() (search/sort/group toggling) never touches pageHead's tree.
  • No XSS: fleetFilter/search term only ever reaches the DOM via text: props (textContent), never html:.
  • CSS custom properties used (--warn, --ok, --accent, --radius, --muted) all already exist elsewhere in app.css; new selectors (.attention-link, .attention-note[.warn|.ok], .attention-control) are all exercised by the test file and syntactically sound.
  • Test file compiles against current FleetServerCard/DarlingFleetReader.BuildRollup/FleetOverviewResult shapes (checked field names, DefaultWorstCount = 5, arithmetic in the two rollup-based tests).
  • Lite/Darling parity: Lite has no web-dashboard JS surface (FleetView.cs is WPF), and the PR description correctly scopes the Lite twin to a separate PR — no gap to flag here.

Nit (non-blocking): when a search term matches zero servers and the attention filter is on, noMatchText() says "No servers needing attention match "term"." even though no server at all (healthy or not) matched the term — technically true (vacuous), just a slightly imprecise message. Not worth blocking on.

No correctness, security, or performance issues found. This is a well-tested, low-risk change.

Review's nit, and it is right. With a search term AND the attention filter both
on, an empty grid always said "No servers needing attention match X" -- which
is vacuously true when X matched nothing at all, and points the reader at the
wrong control. They would go looking for a filter to clear when the answer is
that the term matches no server on the fleet, healthy or otherwise.

The denominator commit already computed the thing that splits the two cases:
searched is the term's result before the toggle sees it. So a zero there blames
the search, a non-zero blames the filter, and the wording says which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@erikdarlingdata

Copy link
Copy Markdown
Owner Author

Took the nit — it is right, and the denominator commit had already computed the thing that fixes it.

With both filters on, an empty grid always said No servers needing attention match “X”. That is vacuously true when X matched nothing at all, and it points the reader at the wrong control: they go looking for a filter to clear when the real answer is that no server on the fleet matches the term.

searched is the split — the term's result before the toggle sees it. Zero there blames the search, non-zero blames the filter:

state line
term matches nothing at all No servers match “nothing-matches”.
term matched, none of them unhealthy No servers matching “warn0” need attention.
no term, filter on, fleet calm No servers need attention.

Harness is 26 assertions now, all passing; the C# pins still split 6 of 7 against dev.

Comment thread Darling/PerformanceMonitor.Darling.Service/wwwroot/js/pages/fleet.js Outdated
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed. This is a well-scoped, well-tested client-side change — no T-SQL involved, and the PR description correctly scopes it to the Darling web dashboard only (Lite gets its own PR, per #2437), so no parity drift to flag here.

What holds up:

  • cardNeedsAttention reads the server-computed band off the card rather than re-deriving a threshold client-side (R1) — confirmed the predicate matches BuildRollup's cards.Where(c => c.Band != FleetHealthBand.Healthy) and the JSON enum token ("Healthy") the shipped serializer actually emits.
  • attentionCountText/wording is verified byte-for-byte against ViewerDataService.Fleet.cs's AttentionFilterCountText, so the desktop/web vocabulary genuinely matches.
  • All new text goes through el()'s text prop (textContent), so the search term and count are never at risk of XSS even though they include user-typed input.
  • onActivate (not onClick) is used consistently for the new "+N more" link and "Show all servers", picking up role=button/tabindex/Enter-Space via util.js's makeActivatable — matches the rest of the page's clickable-div convention.
  • mount()/appendChildren() tolerate the null notice node when the filter is off, so no crash there.

Left two inline comments on fleet.js, both minor/moderate UX-correctness nits rather than functional breakage:

  1. The "no servers to filter" / green "ok" styling fires identically whether the fleet is truly all-healthy or the search term itself matched nothing while the attention filter is on — the green box can read as an all-clear when it isn't.
  2. The new attention-note status div doesn't carry role="status" the way util.js's noticeStrip does for the same kind of live-updating notice, so the count change on toggle isn't announced to screen readers.

Nothing here blocks merging as far as I can tell — both are polish items.

Both from review, and the first one is this PR's own defect turned on itself.

The notice had two colours and three sentences. A search term that matches
nothing leaves the attention filter with nothing to judge, so shown was 0, so
it took the green all-clear arm and read "Needs attention only, matching "xyz"
-- no servers to filter." in the colour that means everything is fine. The
fleet's problem servers were not found healthy; they were never looked at. That
is a colour contradicting its own text, which is the exact finding #2429's
review raised about the amber arm and which I ported without noticing it had a
third case.

So the notice has three states. Amber is a count of servers wanting attention,
green is a real all-clear over a real result set, and neutral is "nothing
matches that term, so no server was judged" -- which says what happened instead
of implying a verdict. The grid line underneath already named the search; the
notice above it was the one still claiming health.

Second: the notice appears and re-words itself with no page load, and carried
no role="status". util.js's noticeStrip is the house idiom for exactly that
kind of non-fatal live notice, so a screen reader now hears the count change
rather than the grid silently shrinking. It matters more here than usual --
this PR is otherwise deliberate about the keyboard path, using onActivate on
both affordances so they are reachable at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed the diff (test + CSS + fleet.js; no SQL/T-SQL in this PR).

Correctness / R1 discipline: cardNeedsAttention reads the server-computed c.band rather than re-deriving a threshold client-side, and it's the same predicate BuildRollup counts additional_problem_count from — verified against the actual enum's JsonStringEnumConverter output (FleetHealthBand.Healthy"Healthy"), not just assumed. attentionOnly = occurs exactly twice (declaration + the one setter), so the toggle and the "+N more" link can't disagree about where the state lives. mount()/appendChildren() in util.js skip null entries, so the [notice, ...] array pattern is safe when notice is null.

Lite/Darling parity: not applicable — Lite has no web dashboard (WPF-only), and the PR description already scopes this to the web half with the Lite equivalent as a separate PR. The desktop Darling.Viewer wording (ViewerDataService.Fleet.cs) matches this file verbatim, and the test file pins both directly.

Security: all new text (search term, sentence, tag names) goes through el()'s text/textContent path — no innerHTML, no new injection surface.

Left one inline note: a copy edge case where "the 1 server monitored is healthy" is reused for a search-narrowed result of 1, which reads as if the whole fleet has one server rather than "one match." Everything else looks solid — nice test coverage pinning the JS against the shipped serializer instead of transcribing it.

Review again, and again on wording I ported without noticing the surface it
landed on is different. AttentionFilterCountText comes from the viewer, whose
Overview has no search box, so "the 1 server monitored is healthy" is simply
true there -- total IS the fleet.

Here total is what the SEARCH left. So on a 57-server fleet narrowed to one
match, the notice read "matching "prod-07" -- the 1 server monitored is
healthy", which claims the fleet holds one server while 56 others exist and
were never looked at. The plural arm had it too: "all 20 servers are healthy"
over a term that matched 20 of 57.

The unnarrowed arms stay verbatim, so the cross-surface vocabulary pin still
holds and still means something. With a term active the sentence says
"matching" instead of "monitored", which is the fact that makes it true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed the diff (Darling web dashboard only — no T-SQL, no Lite changes, both correctly out of scope per the PR description and #2437's split). Verified:

  • cardNeedsAttention reads the server-computed band field rather than re-deriving a threshold client-side (R1 holds), and the new test pins the JS string against the shipped FleetHealthBand serializer output rather than a typed-out literal.
  • The two filters (search, attention) compose correctly — searched (post-search) is consistently used as the denominator for the notice text, and matched (post-attention) is what's rendered, so the notice's count always matches what's on screen.
  • attentionOnly/attentionToggle have a single writer (setAttentionOnly), so the header checkbox and the "+N more" link can't disagree about filter state.
  • el()/mount() only ever set textContent, never innerHTML, so the search term and card data reaching the new notice text can't XSS. mount() handles the null array entries used for the conditional notice.
  • CSS variables used by the new .attention-* rules (--warn, --ok, --bg-dark, --border, --accent, --muted, --radius) all already exist elsewhere in app.css.
  • Cross-checked the new test file's DarlingFleetReader.BuildRollup/FleetServerCard/FleetHealthBand usage against the actual internal API and InternalsVisibleTo wiring — signatures and accessibility line up.

Left one inline finding: when the "Needs attention only" filter empties the flat (non-grouped) grid, the page shows two stacked messages that say the same thing (the attentionNotice banner plus the grid-area noMatchText fallback), which cuts against the PR's own precise-single-sentence-per-state design and diverges from the desktop viewer counterpart, which just leaves the grid blank in that case.

Review, and it lands on the PR's own stated goal. With the filter on and
nothing left to show, the page stacked two boxes: the notice above saying "all
57 servers are healthy", and the grid-area fallback below saying "No servers
need attention." Same fact, different words, one under the other. The desktop
viewer shows only its count and leaves the grid blank.

The notice already explains an empty grid whenever it is showing, and more
precisely -- it names the search term, the population, and whether anything was
judged at all. So the fallback is suppressed under it, leaving the one case the
notice does not cover: the filter off, where the search term is the only thing
that can have emptied the grid.

That collapses noMatchText back to a single question, which is the honest shape
for it. Its three-way split existed only to disambiguate cases the notice now
owns; keeping it would have meant maintaining two answers to a question only one
of them is ever asked. The term-less arm stays, worded honestly, because it is
unreachable today rather than impossible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed the diff (FleetPageAttentionFilterTests.cs, fleet.js, app.css). No blocking issues found.

What I checked

  • Predicate provenance (R1): cardNeedsAttention reads the server-computed c.band field rather than re-deriving a threshold client-side, and the test pins the literal JS comparison string against the actual FleetHealthBand.Healthy JSON token via DarlingFleetReader.JsonOptions rather than a hand-typed string — so a future enum rename breaks the test instead of silently desyncing the filter from the count that links to it.
  • Message parity with the desktop viewer: the shared arms of attentionCountText ("showing X of Y", "all N servers are healthy", "the 1 server monitored is healthy", "no servers to filter") are asserted byte-for-byte equal to ViewerDataService.Fleet.cs's FleetRollup.AttentionFilterCountText, confirmed by reading both files — the extra narrowed arms are correctly scoped to only the web surface, which is the one with a search box.
  • State handling: attentionOnly is intentionally excluded from the localStorage persistence that fleetGrouped/fleetSort get (documented rationale: a triage filter shouldn't silently reopen the page with most servers hidden). attentionToggle is reassigned on every renderFleet() call (every 60s poll), so there's no stale-DOM-reference risk across refreshes.
  • XSS: all new text goes through el(..., {text: ...})textContent, never the banned html prop; the search term is interpolated into a text field only, not markup.
  • Lite/Darling parity: the PR description notes this is intentionally the web-dashboard half only, with a separate PR planned for Lite. I confirmed Lite's FleetView.cs/FleetRow.cs have no equivalent "+N more"/truncated-ranking concept today, so there's nothing currently out of parity to flag.
  • Empty/edge states: walked through zero-total-servers (guarded earlier in renderFleet, before this code is reachable), search-narrowed-to-nothing (searchFoundNothing → neutral "none" color, not the green all-clear), and grouped-view + filter-empties-grid — the notice and the grid-area fallback don't double up in any case I traced.

No T-SQL in this PR, so the collector/style conventions don't apply here.

One non-blocking observation, pre-existing and not introduced by this diff: renderGrouped's own "No servers match / No tagged servers yet" empty-state branch (fleet.js ~line 322) appears unreachable whenever lastTags.length > 0, since buildTagGroups emits a header per tag in the forest even when zero cards fall under it — so filtering (search or now attention-only) to zero results in grouped view still renders a tree of zero-count group headers rather than that fallback message. Not something this PR needs to fix, just noting it's now also reachable via the new attention filter, not only search.

@erikdarlingdata
erikdarlingdata merged commit 12ae1df into dev Aug 21, 2026
9 checks passed
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