Repository navigation
Render the sweep's memory: the Fleet Sweeps page (#3466, lane 3 of 4) - #3472
Conversation
…mute costs and dead instruments stay visible (#3466) Lane 3 of 4, stacked on the V123 state store and the V124 engine: the read-only /api/sweeps surface (dedicated reads like /api/fleet, never tool mirrors — the tool arrives in lane 4 against the same builders) and the Fleet Sweeps dashboard page. The timeline serves caller-configurable spans, default one hour per the owner's ruling, validated through the same McpHelpers.ValidateWindow authority every windowed MCP read applies — refusing rather than clamping or defaulting, and echoing the window it actually served. The watch worklist's default view is open UNION carried in one new store read (open lasts exactly one sweep by design), with any named state reachable by exact spelling and typos refused. FleetSweepPresentation is the shared reshaping layer both presentation surfaces serve: documents embedded as objects rather than re-escaped strings, ledger rows carrying server names joined from the same sweep's verdicts, band transitions pre-computed, and the hysteresis bars riding the payload beside the counters they bound. The honesty rules are restated on the wire and in the render: the mute header on every sweep with the would-have-paged ledger present whenever one was taken under mute — empty included — and absent under alerts-on where an empty array would claim a check never made; and a sweep that could not prove its instruments renders LOUDLY, a red role=alert banner on the document and a marked timeline row, because quiet-is-not-clean applies to CSS too. The cadence knob is display-only here, read from get_alert_settings' fleet_sweep group. Censuses moved: the seat matrix gains the sweep rows (both seats GET, unsafe born-gated), the asset census gains pages/sweeps.js, the rung tests pin the two new store statements, and FleetSweepWebFeedTests pins the span/state refusal tables, every wire shape, and the frontend parity in the AlertTrayStatusLoggedTests idiom (SWEEP_WATCH_STATE_LABELS parsed strictly against the state machine's constants). ExcludedToolNames untouched. The changelog entry lanes 1-2 deferred lands here, the lane that makes the feature visible. Verified out-of-band on macOS: full solution builds clean, and a 53-check harness ran the pure half plus a live TimescaleDB pg17 pass — real engine-written rows served end-to-end by a real Kestrel host through the real Map, refusals, 404s, mute header, joined ledger and watch views all on the wire.
|
Reviewed the diff against Security
Correctness
Parity
Style
Only a non-blocking nit: Nice work — the test coverage ( |
…ose naming the write tool it proves it never calls Two censuses caught the page on its first Windows run: every rollup figure must carry aria-describedby through the shared rollupTextId derivation (#3031/#3045 - the tile closure now lives in the render with a per-render id set, one construction path per page), and the page's own reads-the-cadence-writes-nothing pin found the write tool's name in the header comment. The prose now says what owns the knob without spelling the tool it exists to not call.
ReviewWent through the diff ( Correctness — looks solid:
Security — the new watch-item state filter and the Lite/Darling parity — not applicable here: fleet sweeps are a Darling-only concept (multi-server fleet engine), and I confirmed there's no Style — Postgres-side SQL is parameterized, schema-qualified ( Minor observations (non-blocking):
Nothing else stood out as a bug, drift, or regression. Tests ( 🤖 Generated with Claude Code |
Render the sweep's memory: the web feed at configurable spans, where mute costs and dead instruments stay visible
Lane 3 of #3466's approved four-lane build order, following lanes 1–2, both now merged (
#3470V123,#3471V124 — including the sighting-semantics follow-up this page's "Last seen" column and itslast_seen_at DESCordering depend on: the stamp moves only on sightings, so the column means what it says). What lane 1 persisted and lane 2 writes on cadence, this lane makes VISIBLE: the read-only/api/sweepssurface on the Darling web dashboard and the Fleet Sweeps page that renders it — the per-sweep stateful workhorse the owner's delivery ruling assigned to the web ("the web feed for it should be configurable spans of time though, default hourly"), with channel delivery deliberately absent because it is lane 4's daily-ceiling rollup. This is also the lane the feature becomes user-visible in, so the changelog entry lanes 1–2 deliberately deferred lands here.The endpoints: dedicated reads, like
/api/fleet, not tool mirrorsFour GET routes under
/api/sweeps, mapped fromMapAllbeside the triage endpoint and shaped by the/api/fleet//api/agprecedent — dedicated DTO endpoints, not/api/read/{tool}mirrors, because the sweep has no read tool yet: lane 4'sget_sweep_reportsarrives against the SAME presentation builders these routes serve.ExcludedToolNamesis therefore untouched and the tool-catalog parity pin has nothing of this surface's to count, verified rather than assumed.GET /api/sweeps— the timeline: runs inside the caller's span, newest first, each with its document embedded.hoursdefaults to 1 (the ruling's default-hourly view; at the shipped hourly cadence that lands on the newest sweep with the span control as the way into history) and both knobs go throughMcpHelpers.ValidateWindow— the SAME authority every windowed MCP read applies, so the two surfaces cannot disagree about what a bad span or a futureas_ofanchor means, and the refusal prose is one vocabulary. An out-of-range span is REFUSED, never clamped, and an unreadable one is refused rather than defaulted (the get_collection_log cannot answer the slow-run question: a 200-row newest-first cap makes hours_back irrelevant, with no collector or duration filter and no truncation disclosure #3287 filter discipline: a caller who mistyped a window must not receive a complete-looking answer to a different one). Both resolved bounds are echoed on the response — The analysis family cannot be anchored at a past window, and it is the one an incident starts with #2506's read end: a response that does not say what window it covers invites the caller to assume the one it asked for.GET /api/sweeps/latest— the landing document. 404 with an honest sentence on a store with no sweep yet; a store FAULT is a 500, never a 404 that reads as "no sweeps".GET /api/sweeps/{id}— the timeline click-through. 404 means genuinely absent (pruned, or never recorded); the store read for it throws on a fault for exactly that distinction.GET /api/sweeps/watch-items— the worklist. The DEFAULT view is open ∪ carried (lane 1 review's ruling: "open right now" means the union, because the open state lasts exactly one sweep by design), served by ONE new store read rather than two calls at two instants;?state=narrows to any of the machine's four states by exact spelling, and an unknown state is refused naming the legal values — an empty answer to a typo is indistinguishable from a clean worklist.Seats: every route is a GET, so both seats reach all of it — nothing consults
CanEdit, exactly perDarlingWebSeat.IsRequestAllowed's group-level gate (a sweep report is what the viewer seat exists to grant), and the auth middleware 401s a no-seat caller before any handler runs. The reads run on the host's least-privilege viewer pool, whose blanketcollectSELECT already covers the sweep tables (lane 1's no-ACL rung, verified there).The cadence knob is not touched. The page displays the effective cadence from
/api/read/get_alert_settings'fleet_sweepgroup — the read every settings surface makes — and carries no write affordance at all; lane 2's V124, the Settings window andupdate_alert_settingsown the writes. Pinned as a source census on the page (readsget_alert_settings, contains noapiSend, never names the write tool).The shared presentation layer, so lane 4 cannot fork the wire
FleetSweepPresentationis the reshaping layer the lane brief demanded instead of per-surface forks: the store's reads stay row-shaped (the engine joins them relationally), and the RENDER reshaping happens once — the web routes serve these builders today, and lane 4'sget_sweep_reportsserves the same ones. What the reshaping actually is:report_json, the liveness block and every evidence payload are parsed and embedded as objects (theBuildFullRuleNodedefinition-embedding precedent) — a string would make every client parse a document out of a document, and an agent on lane 4's tool would burn its window on escape characters. A payload that does not parse is carried VERBATIM as a string, failing toward visible rather than toward an absence that looks deliberate.alerts_enabled(the spec's on-every-sweep mute header) andinstruments_aliveunconditionally; the detail shape carrieswould_have_pagedwhenever the sweep ran under master-off INCLUDING EMPTY ("muted, and nothing would have paged" is a statement the operator is owed) and OMITS the key on an alerts-on sweep, where an empty array would claim a check the engine never made.band_changedpre-computed (the diff is the feature's acceptance test, so the wire states it) with the signals evidence embedded beside the band, verdict-beside-inputs.entry_bar_sweeps/exit_bar_sweeps, spliced from the state machine's constants): "1 miss" only means something beside "of 2 to close", and a client that hardcoded 2 would silently mis-render the day per-item thresholds arrive as data (the Retention Held's warn/critical ratios are compile-time constants, so the alert an operator most needs to tune is the one alert that cannot be #3297 route lane 1's docs reserve). The fleet-scope sentinel is named on the wire (fleet_scope) so no client has to know that server id 0 means the fleet.The store gained exactly two reads, both pinned in lane 1's rung file
GetOpenAndCarriedWatchItemsSql/GetOpenAndCarriedWatchItemsAsync— the one-call open ∪ carried default view, both literals spliced from the state machine's constants (the active read's rule), pending and closed deliberately outside it, same ORDER BY as the single-state read so the two views cannot disagree about "newest first". Presentation posture: logs and degrades.GetRunSql/GetSweepAsync— the by-id detail read, sharingRunColumnswith the other two run reads and the one mapper. It THROWS on a store fault, deliberately unlike the span read beside it: this serves the detail route, whose degraded rendering is its own HTTP error body — a fault swallowed into null would be answered as a 404, telling an operator a sweep the store holds does not exist. Null means exactly absent.Both join
FleetSweepStateRungTests' statement census (nonow(), parameterized, the derived state literals — quoted, because the bare wordclosedsits insideclosed_sweep_id).The page: the render carries the feature's contracts
js/pages/sweeps.js+ a#/sweepsroute and nav entry, in the existing idioms (module-level state surviving the 60s poll like the fleet page's sort,el()/textContent only per R4, pre-judged values rendered and never re-derived per R1). The layout: the sweep document (latest, or the clicked sweep), the timeline at the configured span, and the watch-item worklist.role="status"banner) and the would-have-paged ledger renders whenever the document carries one — including empty, with the sentence saying the mute cost nothing this sweep.instruments_alive: falseis a redrole="alert"banner on the document, a red edge on the document card, and a red edge +INSTRUMENTS DEADbadge on the timeline row — a dead-instrument sweep cannot be mistaken for a green one at any zoom.Censuses moved, tests, verification
DarlingWebOidcTests.IsRequestAllowed_Matrixgains the sweep rows: both seats GET everything,POST /api/sweepsborn-gated.DarlingWebAssetsTestspinsjs/pages/sweeps.jsinto the build-output census.FleetSweepStateRungTestspins the two new statements (above).FleetSweepWebFeedTests(new): the span refusal table (unreadable / zero / negative / over-max hours, bad and future anchors — through the shared MCP authority), the watch-state list == the machine's constants with wrong-case refused, the default-span-is-one-hour ruling, every presentation shape above (mute header and liveness always present, documents embedded, unparseable-carried-verbatim, ledger present-empty-under-mute and absent-under-alerts-on, names joined with honest nulls,band_changedtruth table, bars and fleet-scope on the watch payload, window echo) — and the frontend parity pins in theAlertTrayStatusLoggedTestsidiom, JS being untested by convention:SWEEP_WATCH_STATE_LABELSparsed strictly and compared key-by-key to the state machine's constants, the dead-instrumentsrole="alert"branch and the ledger-whenever-carried branch located by source, the bars-not-hardcoded pin, the cadence-is-display-only pin, and the shell/router wiring pin.ExcludedToolNamesuntouched; the/api/readparity pin unaffected (no new tool this lane). README's web-dashboard sentence and Darling/README's "What you see" blurb gain the Fleet Sweeps page; the CHANGELOG entry for the whole feature lands here per lanes 1–2's stated disposition (the lane that makes it visible owns the entry).Development on macOS, where the test assemblies build but cannot run, so verification was: the full solution builds clean (0 errors; all warnings pre-existing in untouched files), and an out-of-band harness referencing the built Service assembly ran 53 checks — every pure binding/presentation behaviour above (the internal endpoint helpers reached by reflection), plus the live half against a real TimescaleDB pg17: full-ladder migration, a REAL
FleetSweepEngine.RunAsyncsweep plus a planted master-off sweep with watch items in all four states, the two new store reads over those rows (by-id round-trip and null-on-absent, open ∪ carried returning exactly the open and carried rows while the closed row stays reachable by name), and the routes served end-to-end by a real Kestrel host mapped through the realDarlingFleetSweepEndpoints.Map— the empty store's honest 404-latest and empty-window timeline, both sweeps on the wire with embedded documents, every 400 refusal in the shared prose, the mute header and name-joined ledger onlatest, the ledger key absent on the alerts-on sweep, and the watch default/named/refused triple. All pass.What lane 4 consumes from this PR
FleetSweepPresentation—get_sweep_reportsserializes the SAME builders these routes serve (BuildTimelineNode/BuildSweepDetailNode/BuildWatchItemsNode), so the MCP surface and the web feed cannot drift; the embedded-documents rule is exactly what an agent consumer needs.GetOpenAndCarriedWatchItemsAsyncis the "open right now" read for the rollup's watch-transitions section, andGetSweepAsyncthe anchor for citing a specific sweep.would_have_pagedpresent-iff-muted,band_changed,fleet_scope, and the bars-on-the-payload contract are now pinned shapes lane 4 inherits rather than decisions it re-makes.Part of #3466 — depends on lane 1 (
feat/3466-sweep-state-store, V123) and lane 2 (feat/3466-sweep-engine, V124); lane 4 (daily channel rollup ceiling +get_sweep_reports) follows against these shapes.