fix(fleet): resolve Stop-by-label stack labels across all nodes (#1382)

* fix(fleet): resolve Stop-by-label stack labels across all nodes

The Fleet Actions "Stop by label" card only ever saw the control node's own
stack labels. The stack-label routes are proxied, so each node stores its
labels in its own database and the control holds no mirror for remote nodes.
The suggestions, match-preview, and fleet-stop endpoints all read that
nonexistent mirror, so remote-only labels were invisible and remote stacks
were skipped before the remote was ever asked.

Make all three authoritative across the fleet with a new
collectFleetLabelSummaries helper: the local node reads its own database,
and each remote is queried live through its labels and label-assignments
endpoints over the proxy, with fail-closed parsing and per-node
reachability. fleet-stop drops the mirror pre-check and always calls each
reachable remote's local-stop receiver, reporting unreachable nodes at the
node level so they never block the reachable ones.

The picker now surfaces remote-only labels, aggregates shared names once
with combined counts and the carrying node names, and flags incomplete
coverage when a node is unreachable. The preview groups matches per node,
lists unreachable nodes separately, and distinguishes no matching stacks
from "label exists but no stacks" from "remote unavailable".

* fix(fleet): harden Stop-by-label card against malformed responses

Guard the match-preview and fleet-stop response bodies so a malformed but
200 reply degrades instead of crashing or misreporting. match-preview now
validates the per-node shape before rendering (the preview reads it outside
any try/catch) and logs a malformed body; fleet-stop distinguishes a
non-array results body (a server bug, now logged and surfaced as an
unexpected-response error) from a genuine empty fleet, and guards per-node
stackResults.

Docs: an unreachable or errored remote is reported once per node, not as a
per-stack error row.
This commit is contained in:
Anso
2026-06-17 13:25:12 -04:00
committed by GitHub
parent f166a537c3
commit cb58cc423f
8 changed files with 827 additions and 163 deletions
+3 -3
View File
@@ -93,9 +93,9 @@ Two cards in the **Fleet · Actions** tab use labels to drive cross-node operati
### Stop fleet by label
Type a stack label name; Sencho fans the request out to every node and stops every stack on that node assigned a stack label with the same name. This action targets stack labels only, never node labels. The picker suggests stack labels from across the fleet, read from the control instance so labels on offline nodes appear too, each with its stack and node counts so the scope is unmistakable. The result list shows a per-node breakdown with success and failure counts, and the helper line under the input states the scope: `Stops stacks assigned to this stack label across matching nodes. Node labels are not used by this action.` A confirmation modal titled `Stop all stacks with the stack label "<name>"?` with the **Stop fleet** primary action runs the action.
Type a stack label name; Sencho fans the request out to every node and stops every stack on that node assigned a stack label with the same name. This action targets stack labels only, never node labels. The picker queries each reachable node for its own stack labels, so a label that exists only on a remote node still appears, listed once with its combined stack and node counts (and the carrying node names) so the scope is unmistakable. When a node cannot be reached, the picker notes that the suggestions may be incomplete. The result list shows a per-node breakdown with success and failure counts, and the helper line under the input states the scope: `Stops every stack assigned this stack label on every reachable node across the fleet. Node labels are not used by this action.` A confirmation modal titled `Stop all stacks with the stack label "<name>"?` with the **Stop fleet** primary action runs the action.
A node with no stack label by that name is reported as a failure during the run, so a partial-fleet stop is observable rather than silent.
A node that carries no matching stack label is shown as such, and a node Sencho could not reach is reported as unreachable, so a partial-fleet stop is observable rather than silent. Unreachable nodes never block the stop on the reachable ones.
### Bulk label assign
@@ -129,7 +129,7 @@ The **Bulk label assign** card is per-node only by design. To re-tag stacks on a
The Tags filter aggregates labels across every node in the fleet by name. If the new label only exists on one node and that node was offline at the moment the page loaded, the dropdown may not include it. Refresh **Fleet · Overview** with the **Refresh** button in the toolbar to repull node state.
</Accordion>
<Accordion title="`Stop fleet by label` reports `No node carries a stack label by that name`">
Stack labels are per-node, so the fleet-stop matches by name across nodes. If the stack label you typed only exists on one node and you typed the wrong case (`prod` versus `Prod`), no node will match. The picker suggests stack labels read from the control instance across every configured node, including offline ones; node labels never appear there. Pick from the suggestion list rather than typing freehand to avoid case mistakes.
Stack labels are per-node, so the fleet-stop matches by name across nodes. If the stack label you typed only exists on one node and you typed the wrong case (`prod` versus `Prod`), no node will match. The picker queries each reachable node for its own stack labels; node labels never appear there. A label on a node Sencho cannot currently reach will not be suggested, and the picker flags that the list may be incomplete. Pick from the suggestion list rather than typing freehand to avoid case mistakes.
</Accordion>
<Accordion title="`Bulk label assign` cleared every label on my stacks unexpectedly">
The card replaces, it does not merge. Selecting no labels and clicking **Apply to N stack(s)** is the documented way to clear assignments, and the confirmation copy on the **Bulk label assign** modal restates this: `No labels selected, this will clear existing assignments on the selected stacks.` Re-pick the labels you want and run the action again to restore them.