mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-10-25 21:20:25 +00:00
1efaa13492
* refactor(stacks,security): extract status and overview builders into services
Move the bodies of `GET /api/stacks/statuses` and `GET /api/security/overview`
into `services/stackStatusesEvidence.ts` and `services/securityOverview.ts`.
Both routes keep their guards, response shapes, error handling, and timing
logs; they now delegate to the new builders.
The moved logic is verbatim apart from `req.nodeId` becoming a `nodeId`
parameter and `res.json(...)` becoming a return, so the existing route suites
cover it unchanged.
The move does restructure one thing: a builder rejection is now caught by a
`try` in the route that no longer wraps the code which throws. That propagation
was not covered by any test, so `cache-endpoints.test.ts` now pins it, including
that the real error still reaches the console rather than the 500 standing alone.
This is groundwork: the fleet readiness surface needs to read the same evidence
from a batch context rather than through one Express request per node.
Extracting first keeps that change from also being a refactor.
One accepted deviation, on the error path only. `dockerMs` and `enrichmentMs`
were route-scope bindings mutated by the cache closure, so a rejection after a
timing was recorded still let the route log that partial value. They are now
builder-scope, so a rejection logs the initial nulls instead. The HTTP
response is unchanged (500, same message); only the debug timing line differs,
and only when an unexpected throw lands between the Docker call and the
enrichment block with no stale cache entry present.
* feat(readiness): add the fleet readiness wire contract
Define the types the fleet readiness surface speaks in: the six domains,
the per-node domain cells the matrix renders, the prioritized findings
list, and the per-stack summary the tier-2 node route returns. No producer
or consumer lands in this commit; the routes and the aggregator follow.
The contract carries no scoring and no prose headlines. A finding's display
payload is a reason code the frontend maps to copy, and the two free-text
fields (finding detail and per-stack top reason) carry an explicit
redaction requirement, because the canonical contracts they pass through
can embed a materialized input path.
Also exports the reachability classifier from the blueprint preview
projection, along with the contact-source union it already returned and a
closed union for its note, so the fleet surface classifies reachability
with the same cached logic rather than writing a second classifier. The
three existing consumers are unaffected: both substitutions are in type
positions only, and 39 tests across the preview, binding-preview, and
reconciler-gate suites pass unchanged.
* feat(readiness): add the node-local per-stack readiness rollup
Adds `GET /api/stacks/readiness-summary`, the node-local half of the fleet
readiness surface. It reports the canonical per-stack update and rollback
verdicts for one node, because the Docker socket is on that node: the update
preview's remote path aborts at a hard 90s ceiling per stack, which a hub
fan-out cannot wait on.
The route adds no scoring. It selects, bounds, and reports what
`UpdateGuardService` already computes:
- one pass, aborted by a 6s deadline that starts before the compose directory
scan so the scan is charged to the same budget
- concurrency 3, and a 25-stack cap that keeps the most operationally
interesting stacks (failed checks, then updates available, then open drift
findings, then alphabetical)
- a 120s cache entry, invalidated on stack mutation through
`invalidateNodeCaches` so a fan-out does not re-run the Docker-heavy pass per
request
- the two verdicts settle independently, so a failing rollback compute cannot
erase a good update verdict; a stack whose work throws degrades to an
unavailable row rather than failing the pass
Each row carries the strongest reason behind its verdict, taken from the
canonical signals and items and passed through `redactSensitiveText`, because a
detail there can carry a materialized input path on a `node:read`-gated surface.
The served payload carries `stale`, derived from the pass timestamp rather than
from the cache outcome. The outcome is not truthful for a caller that joined
someone else's in-flight fetch: when that fetch fails and an expired entry is
served, the caller that ran it is told `stale` while every joiner is told
`inflight`, on the same old bytes. An expired entry's own timestamp names the
condition for every reader. The pass type omits the field, so a pass cannot
assert freshness it cannot know.
The contract also names the aggregate's row types (`StackReadinessRow`,
`StackUpdateReadinessRow`, `StackRollbackReadinessRow`), so the producer and its
tests refer to them rather than restating the shape at each call site.
Two registrations move together and each has its own test: the route is mounted
ahead of the `/:stackName` catch-all, and `/stacks/readiness-summary` joins
`STATIC_STACK_PATHS`. Missing the first resolves the request as a stack named
`readiness-summary`; missing the second demands exact `stack:read` on a stack of
that name, for the router gate and for the hub's own fan-out call.
`fleet-readiness-v1` joins the capability registry on both sides, following the
append-only convention there. The hub reader that gates on it lands with the
aggregator.
The route is guarded by `stack:read`, which every role holding `node:read`
already has.
* feat(fleet): aggregate fleet readiness on the hub
Add GET /api/fleet/readiness, a hub-owned aggregate over six domains
(connectivity, workloads, updates, recovery, security, control) that answers
what across the fleet needs attention before an operator acts on it.
The hub answers from its own SQLite for connectivity, stored update facts,
snapshot gaps, and Policy Sync state, and fans out two bounded node-local
reads: GET /api/readiness/evidence (3s budget) for workloads, security, and
stored recovery rows, and GET /api/stacks/readiness-summary (8s budget) for
the canonical per-stack update and rollback verdicts. Every node is wrapped
in its own try/catch into a per-node row, so one slow or unreachable node
degrades one row instead of holding the page, and a disconnected Pilot
target costs no request at all. A build without the evidence route answers
404, which is reported as a missing capability rather than as a fault.
Findings carry a stable id, an enumerated code, and a drill-down target.
Verdicts are read from the existing update and rollback readiness functions
rather than recomputed here, and nothing is scored: a check that could not
run is reported unknown or unavailable, never healthy.
The Control domain is admin-only, matching the sibling Policy Sync read. The
response omits the domain for an account that cannot see it rather than
sending it and relying on the client to hide it.
Retire GET /api/fleet/configuration and the backend normalizer that served
only it. Home's Configuration Summary keeps its own builder, and
GET /api/dashboard/configuration is unchanged.
Document the three endpoints in docs/openapi.yaml.
* feat(fleet): replace the Status tab with Readiness
Swap the Fleet tab strip's configuration inventory for a Readiness tab: a
worst-first fleet summary, a prioritized findings worklist, and a per-node
matrix over the six readiness domains. States, reason codes, and findings
all arrive decided from the hub, so the surface renders the payload and
dispatches each drill-down to the surface that already owns that
remediation.
The matrix lays out the domains the payload carries, so the Control column
is simply absent for an account the server withheld it from. Tier-2 columns
are read from each node's own cells rather than from hasCapability(), which
describes the active node and would misreport every other row.
Findings that restate an update or a rollback verdict reuse the mappings
those surfaces already publish instead of re-declaring them.
Normalize the legacy `configuration` segment to `readiness` in the route
parser so an existing deep link still resolves.
Delete the retired configuration inventory component.
* docs(fleet): document the Readiness tab
Follow the tab from Status to Readiness on the Fleet view page: the summary,
the findings worklist, the per-domain matrix, what happens when a node cannot
be reached, and the admin-only Control column. Update the tab table and the
tab-strip prose, and drop the tab list from the Overview screenshot alt text
so it stops describing a strip the screenshot no longer shows.
Replace the tab screenshot and follow the rename through Fleet Sync, the
deep-link reference, the introduction, and the API reference contents.
* test(fleet): compare readiness mock hosts by origin
The readiness fan-out tests route each mocked fetch by asking whether the
request targeted the proxy host. Decide that on the parsed origin rather
than on a prefix match against the raw URL string, which is the shape the
static analysis flags as incomplete URL handling.
No behavior change: every URL reaching a dispatcher is absolute, and each
of the four tests exercises both sides of the branch.
* fix(readiness): keep ordinary configuration and repeated causes out of findings
The aggregate reported several conditions that are not problems, and one
unreachable node could hold its row for the full verdict budget.
- A stack with no containers (not deployed) is a known state, not missing
evidence: it is no longer a Workloads concern, and its update and
rollback verdicts are left out. A stale or degraded status read never
hides a verdict.
- A fleet that never took a snapshot adds no Recovery concern; snapshots
are optional. An unreadable skip column is a check that could not run.
- Policy sync (Control) is classified only for the nodes it pushes to.
The hub's own node and Pilot-agent nodes carry no Control cell instead
of a permanent "unknown".
- When the reachability probe gets no answer, the slower verdict read to
the same remote is cancelled, so a dead remote costs the 3s probe
budget instead of the 8s verdict budget. A verdict read that failed on
its own keeps its own cause.
- When Connectivity already reports why a node could not be read, the
gated domains keep their cells but add no finding restating the same
cause. The hub's own node, whose Connectivity is always healthy, keeps
every finding.
- Rows are sorted worst first and carry their worst state.
* feat(fleet): rebuild the Readiness tab on the shared page patterns
The tab used a bespoke layout that did not match the rest of the app. It
now uses the same building blocks as the other Fleet tabs and the
Security and GitOps tables:
- The shared Fleet tab heading, then a summary strip with the worst state
as a short phrase and tiles for nodes needing attention, unverified,
healthy, and when the check last ran.
- Findings on the shared table shell with the standard filter row
(search, domain, state, verdict, node) and pagination. The verdict
filter has presets for rollback partial or not ready, and for updates
that are blocked or need review.
- A node matrix whose cells name each state in the domain's own words
("online", "2 blocked", "never scanned", "in sync", "--" where policy
sync does not apply) instead of generic state words. Clicking a problem
cell narrows the findings to it.
- The Fleet toolbar Refresh now re-runs the readiness check.
- A Security finding switches to the finding's node before opening
Security, using the same pending node-view intent the Networking
shortcut uses.
* docs(fleet): describe the rebuilt Readiness tab
Rewrite the Readiness section for the new summary strip, findings
filters, and domain-specific matrix values, refresh the screenshot, and
update the Fleet Sync references and the OpenAPI node contract (worst
state per row, Control only for policy sync targets, one finding per
unreachable node).
160 KiB
160 KiB