* feat: add service-scoped stack alert rules
Stack alerts can target one Compose service or all services. Breach timers
are per container and cooldowns are per service so a healthy sibling no
longer clears another container's timer or silences a different service.
* fix: gate remote scoped alert creates without losing the body
Remote hops skip JSON parsing so the proxy stream stays pipeable, which
left service_name invisible to the capability gate. Buffer POST /alerts
bodies for inspection, fail closed when the remote lacks the capability,
and rewrite the buffered bytes on forward. Restore alert-panel alt text
to match the unchanged screenshot.
* fix: bound remote alert body buffer and reject encoded JSON
Cap proxied POST /alerts buffering at the local 100KB JSON limit with
structured 413 cleanup, reject non-identity Content-Encoding with 415 so
compressed scoped bodies cannot bypass the mixed-version gate, and cover
oversized, chunked, and gzip regressions.
* fix: harden service-scoped alert delete, cooldown, and proxy gates
Reject non-digit alert ids, dual-write last_fired_at for rollback safety,
gate cooldown on persisted notification history, fail-fast oversized proxy
bodies with 413, and clarify Not in compose UI semantics.
* test: expect dispatchAlert persisted result in crash-safety cases
Update notification-routing assertions for the new { persisted } return
shape so CI matches the cooldown-gating contract.
* feat: graduate Host Console to Community admins
Make Host Console available to Community and Admiral admins (system:console), add host-console-community for mixed fleets, and keep opaque API tokens off the host shell.
* docs: document Host Console deep links
Cover root and stack-scoped Console URLs, correct the phone treatment note, and pin parse/build round-trips in senchoRoute tests.
* fix: bind Host Console socket to the resolved node
Treat unresolved activeNode as loading, target the WebSocket with an explicit nodeId, and wait for stack deep-link hydration so the shell cannot open on the wrong node or compose root. Add regression coverage for node/stack retargeting and fail-closed directory resolution.
* fix: harden Host Console node binding, audit acting_as, and console_session tokens
Reject unknown or malformed nodeIds before spawning a PTY. Record hub operators in audit_log.acting_as for remote console_session bridges. Path-scope and one-time-consume console_session JWTs so Host Console mints cannot open container exec or be replayed.
* test: expect acting_as in audit CSV export header
Align the CSV export assertion with the P0-2B acting_as column added to audit log exports.
* feat: add weekly UTC maintenance windows to mute rules
Let mute rules suppress only during recurring UTC windows, normalize
replica node identity, and fail-open when remotes lack schedule support
so older nodes never keep an all-day scheduled mute after a successful cleanup DELETE.
* fix: fail closed on corrupt mute schedules and clean invalid replicas
Empty or whitespace stored schedules no longer act as all-day mutes. Invalid schedules trigger remote DELETE cleanup, and the weekly-window form gains accessibility attributes plus component coverage.
* fix: require explicit repair before clearing a corrupt mute schedule
The suppression engine already fails closed on an unreadable stored
schedule (scheduleInvalid), but the frontend never surfaced that flag:
a corrupt rule looked identical to an ordinary unscheduled one, and
opening Edit then clicking Update sent an explicit schedule: null,
silently turning the corruption into a valid all-day mute. Add the
flag to the rule type, show an Invalid schedule badge on the card, and
block saving in the edit form until the operator explicitly touches
the weekly window (configures a new one, or toggles it to confirm the
clear).
* fix: correct contradictory toggle-sequence copy in schedule-repair toast
The blocking toast told operators to toggle the weekly window "off then
on" to confirm clearing a corrupt schedule, but the toggle starts off
for a corrupt rule, so that sequence leaves it on and trips the
no-selected-day validation instead. The correct, tested sequence is on
then off, matching the inline hint below the toggle. Also add a
regression test confirming the invalid-schedule save gate resets
cleanly across edit sessions on different rules.
* fix: enforce replica node_id and guard fleet sync against stale writes
Two hardenings to the suppression-rule fleet sync path found during
review: the /replica endpoint trusted the payload's node_id instead of
forcing it to null server-side, so a direct proxy-authenticated caller
could persist a scoped replica; and upsertNotificationSuppressionRuleReplica
overwrote unconditionally with no ordering check, so a delayed older
POST arriving after a newer one could downgrade the stored rule. Force
node_id to null on every replica write, and skip (with a warning log)
any incoming write whose updated_at is not newer than what's stored.
* test: assert the exact-tie updated_at case in the fleet sync stale-write guard
The staleness guard added in c31458a1 uses >= (ties are ignored, not
just strictly older writes); add the missing assertion for that
boundary and make the comment explicit about it.
* fix: bump vulnerable transitive backend dependencies
npm audit flagged body-parser, fast-uri, and protobufjs (one high
severity: fast-uri host confusion via failed IDN canonicalization).
All three have patch/minor fixes within existing semver ranges;
npm audit fix resolves all three with no package.json changes.
* fix: sanitize suppression replica fields before logging
Log entries built from fleet-sync replica payloads embedded rule id
and timestamp values directly, allowing a compromised peer to forge
log lines via control characters.
* fix: prevent delayed replica writes from resurrecting deleted mute rules
A network-reordered replica POST arriving after a DELETE fell into the
insert-when-absent branch with no protection, since the staleness guard
only compares against a row that still exists. Add a permanent
per-id tombstone (safe because rule ids are AUTOINCREMENT and never
reused): every delete records one, and the replica upsert refuses to
recreate a tombstoned id regardless of the incoming updated_at.
createStack scaffolds compose.yaml; Blueprint was writing docker-compose.yml, so Compose discovery ran the nginx boilerplate. Align Blueprint writes with the canonical filename, clear alternate root Compose siblings on local/modern apply, and cover the regression paths.
* fix(blueprints): gate confirmed apply on live intent fingerprint
reconcileConfirmedPlan only checked approval_status, so a concurrent compose
edit during Apply could deploy unapproved content under a stale fingerprint.
Match the tick-path fingerprint gate, refuse Apply when live intent drifts,
and surface reconciler refusal as PREVIEW_STALE instead of a false success.
* test(blueprints): cover matching-fingerprint reconcileConfirmedPlan path
Prove the production allow branch still deploys authorized actions when the
approval fingerprint matches, and does not execute unauthorized blast nodes.
* fix(blueprints): report live approval after confirmed snapshot apply
A concurrent edit can clear approval while multi-node snapshot deploy is
still running. Keep the in-flight snapshot contract, but re-read live
effectiveApproval before responding and warn in the rollout dialog when
approval is no longer current.
* fix: keep running containers until stack pull/build succeeds
Acquire images before reconcile, capture a recovery generation for
compensation, and only remove classified orphans after handoff.
* fix: address recovery audit blockers for safe stack updates
Retire abandoned and expired recovery artifacts, probe compensated
runtimes before reporting rollback success, preserve local Docker when
deleting a node, validate the exact Compose invocation before capture,
and repair updateStack return-contract fixtures.
* fix: resolve ESLint errors blocking CI on this branch
Unused-import and unused-variable errors left over from the stack
deletion refactor: MeshService in stacks.ts (its opt-out cascade moved
into DeployedStackDeletionService), a redundant pruneVolumes
destructure in deleteDeployedStack (the real one is re-derived from
the same input object inside runDeletionBody), and an unused beforeAll
import in a Docker-integration test stub. Also scopes the webhook
pull-action case body in a block to satisfy no-case-declarations;
purely syntactic, no behavior change.
* fix: harden recovery probe, cleanup retry, and failed-pull Docker test
Reject absent or unhealthy expected replicas before reporting rollback
success, keep cleanup records until artifacts are actually removed, fail
closed when a mesh override cannot be generated, and assert a real
failed pull leaves the original container running.
* fix: verify recovery probe image identity and stack-scoped override paths
Reject recovered runtimes that use the wrong image or leave scale-zero
services running, and confine tombstone override deletion to the intent
stack directory so forged cross-stack paths cannot be swept.
* test: batch notification cap fixtures in a SQLite transaction
Unbatched 1200-row inserts were timing out at the default 30s under
CI load even though the same assertions pass in under 2s when green.
* feat: add configurable notification dispatch retries
Allow each node to set 0-3 extra in-process delivery attempts with a fixed
one-second delay for routes, agents, and Test dispatch.
* fix: harden notification retry settings load/save and channel tests
Guard Delivery retries against failed and out-of-order same-node settings responses, and cover Slack/webhook retry classification alongside Discord.
* fix: clear Delivery retries saving state and correct screenshot alt
Separate save-request ownership from value-generation invalidation so a successful PATCH cannot leave Save retries stuck on Saving, reset saving on node switch, and align the Channels screenshot alt with the committed image.
* fix: surface invalid notification retry settings instead of false saved clamp
Align Channels GET handling with the backend strict 0-3 parser so stored values like 9 or 1.5 show as error needing repair, matching runtime fallback to 0 instead of displaying a clamped saved policy.
* fix(fleet): isolate corrupt snapshot file decrypt failures
A single damaged encrypted fleet-snapshot row no longer fails detail, restore, or off-site upload for the whole snapshot. Unavailable members are marked, restore is blocked before mutation, and cloud upload fails closed with no PutObject.
* fix(fleet): fail closed on damaged enc snapshot envelopes
Unrecognized enc: payloads no longer fall through as usable plaintext. Only clear legacy prose stays readable; delimiter-byte and similar envelope damage stays unavailable through restore and cloud upload.
* fix(fleet): subordinate legacy enc prose to envelope shape
Legacy exceptions no longer trigger from = or whitespace alone. Encryption-shaped payloads (length and hex density) stay unavailable through restore and cloud upload, while short genuine prose such as enc:hello remains usable.
* fix(fleet): preserve non-envelope enc legacy plaintext
Any non-empty enc: payload that is not encryption-shaped is kept verbatim, including punctuation forms such as enc:hello-world, while envelope-shaped damage remains unavailable.
* feat(blueprints): require confirmed rollout preview before reconcile
Persist place/remove approval with an intent fingerprint and transition matrix so Apply, Retry, ticks, and pin cannot mutate the fleet until the operator confirms the current blast radius. Preview surfaces requirements, health, and informational in-flight rows without executing them.
* fix(blueprints): silence unused retry nodeId lint error
* test(blueprints): harden approval gate coverage and preview clarity
Add real reconcileOne place/remove fan-out and STALE_GUARD regressions, surface reachability in the rollout dialog, align warning totals, and document the fail-closed upgrade pause.
* test(blueprints): cover legacy approval schema migration
Seed a pre-approval database with an enabled Blueprint and live deployment, run production DatabaseService startup, and assert pending null auth columns plus a fail-closed reconcile gate.
* test(blueprints): clarify legacy approval migration fixture
Extract seed/boot helpers so the migration regression reads as a linear upgrade path without changing assertions.
* fix(blueprints): report apply outcomes and gate manual withdraw
Return per-node reconcile outcomes from Confirm Apply, block create preview on unmanaged same-name stacks, and require an approved remove outcome for every manual withdraw or evict.
* fix(blueprints): scope withdraw approval to destructive eviction
Require remove approval only for snapshot/evict confirms and evict_blocked rows. Keep plain stateless standard withdraw as an immediate stop, and update withdraw-route tests to seed remove approval when needed.
* feat: add service-scoped Compose update and restore
Allow updating or rebuilding one declared Compose service on multi-service
stacks without recreating siblings, with recovery snapshots, health-gate
observation, and prune holds for rollback images. Full-stack update paths
and single-service UX stay unchanged.
* fix: sanitize service-scoped update log messages for CodeQL
* fix: address service-scoped update audit findings B-01 through B-07
* fix: complete service-scoped update audit metadata and surfaces
* test: wrap Updates readiness tests for deploy-feedback context
* fix: keep service recovery reachable without Deploy Progress
Make failed service-gate recovery discoverable when Deploy Progress is
disabled or dismissed, suppress stale image-scan notification side
effects, normalize ComposeService line endings, and add focused
regression coverage.
* fix: resurface ContainersHealth density and expand on multi-service stacks
Service grouping hid the summary strip and Compact/Detailed/Expand controls that still applied to multi-container stacks.
* feat: add Apprise as a fourth notification channel
Support keyed and stateless Apprise endpoints with secret-safe public DTOs, fail-closed malformed config, and mode-specific Settings UI. Docs and screenshots updated for four-channel Channels and routing.
* fix: harden Apprise secrets at rest and preserve-on-write saves
Encrypt Apprise endpoint and config with CryptoService so a downgrade cannot leak via SELECT *. Align channel and routing saves so blank destination fields omit config on same-mode URL edits, enforce keyed notify IDs, and keep secrets_redacted truthful.
* fix: harden Apprise route type changes and mixed-version config UI
Require a raw channel_url when switching notification-route types so ciphertext cannot strand under Discord/Slack/webhook. Default missing remote apprise status, replace Channels state on node switch, and exercise the production config-column migrator.
* fix: tolerate stub fleet configuration payloads without agents
Normalize remote Apprise agent status only when notifications.agents is present so successful Pilot/stub fetches stay online instead of throwing into the offline catch path.
* fix: correct TypeScript in configuration normalize tests
* fix: ignore stale Channels agent bodies after node switch
Compare the active node after response JSON parsing so a slow body
cannot overwrite the newly selected node's channel state.
* fix: isolate corrupt Apprise crypto and keep keyed Tags visible
Decrypt failures on one Apprise row no longer 500 agent/route lists or
suppress sibling channel dispatch. Treat public /notify/<redacted> as keyed
so Tags remain editable after reload.
* feat: guide missing external network creation during deploy
Detect missing external networks before Compose runs, prompt or auto-create safe bridge networks, and keep unsupported declarations blocked with trusted deploy provenance.
* test: align deploy context and settings fixtures with missing-network gate
Update caller spies, EffResource expectations, StacksSection save keys, and git-source spy cleanup so CI matches the new deployStack context and auto-create setting.
* fix: drop unused renderError binding in missing-network resolver
Satisfies no-unused-vars so backend ESLint CI passes; callers already key only on model presence.
* fix: use HTTP-safe clipboard helper in missing-network dialog
navigator.clipboard fails on plain HTTP LAN hosts; route copy actions through copyToClipboard so Docker and Compose copy buttons work on self-hosted instances.
* fix: simplify missing-network dialog actions and copy label
Drop the Compose snippet escape hatch, move secondary actions under More, and rename the terminal copy action to Copy create command so the footer is a clear Cancel / Create decision.
* feat: add node-scoped Networking operator page
Adds a Networking view with overview, topology, inventory, and findings.
Shared aggregate reads back the page; Resources keeps prune and redirects here.
Includes fail-closed network delete guards, operator docs, and /nodes/:slug/networking routing.
* fix: rename unused variable n to _n to satisfy no-unused-vars lint
* fix: keep top bar search clickable when nav grows
* feat: complete Compose-first Networking Phase 2 operator assistant
* fix: move networking action visibility helper out of component module
* feat(networking): complete Compose-first Networking operator page
Finish the node-scoped Networking page (Overview, Networks, Topology,
Findings) with design-system parity and correct finding semantics.
- Rebuild detail sheets on SystemSheet/SheetSection; align the tab band,
masthead, and mobile tone with Fleet and Security.
- Encode the host-mode and exposure severity matrix; fix collision counts
so intentional shared externals are not flagged; add one typed drift
predicate shared by inventory, topology, badges, and overview counts.
- Preserve per-container attachments and IPs on topology node clicks;
drawer-only click with an explicit logs action; ownership and boolean
filters; bound large graphs before layout.
- Aggregate cached Compose Doctor findings into the Findings tab with
honest source labels, structural merge and dedupe, staleness
reconciliation, and a shared exposure-context helper both engines use.
- Networks tab: privacy-safe service search, precise ownership counts,
schema v3 with version-2 adapters on every endpoint, pre-confirm delete
reasons, and the shared sortable table with an internal scroll region.
- Interop: Fleet node-card networking signal with pending-intent
navigation, stack-to-node backlink, and Dossier/Drift deep links.
- Enrich sanitized inspect with an allowlisted connected-container list;
fetch topology once and filter client-side.
- Docs and tests across every new finding kind, adapter, and flow.
* fix(networking): correct drift count, exposure fail-soft, and inspect crash paths
Address code-review findings on the Networking page implementation:
- Fix the Overview drift count to use the shared drift-kind predicate instead
of a hardcoded list that omitted external-network-missing.
- Gate Compose Doctor's unclassified-exposure and reverse-proxy-undocumented
rules on exposure-context availability, so a DB read failure no longer
fabricates findings (mirrors the live engine's existing fail-soft behavior).
- Guard the per-stack exposure-intent read in topology aggregation so a
transient DB failure degrades to unknown intent instead of failing the
whole response.
- Harden the network detail drawer against a partial inspect payload from an
older remote node, and log the real error instead of a bare catch.
- Remove now-duplicated severity-rank and drift-kind helpers in favor of the
shared modules; drop dead backend-only exports; widen the frontend schema
version type to a plain number instead of casting past a literal type.
- Add coverage for the delete-guard precedence, the full host-mode severity
matrix, the schema-2 compatibility adapter, and the sanitized connected-
container allowlist; tighten two tests that were not exercising the
behavior they claimed to.
* fix: add missing onOpenNodeNetworking prop to FleetView experimental test
The added required prop on FleetViewProps broke the merge-build when the
test file (on main but not on this branch) was compiled against the
updated FleetView interface.
* feat: add developer-mode startup and stack hydration timing
Instrument boot-to-list and detail hydration with commit-aligned milestones, truthful request stages, and destination/gateway debug duration logs so performance work is guided by measurements.
* fix: redact stack names and complete hydration request stages
Stop logging stack identifiers in containers debug timing, and record state_dispatch (plus detail fetch spans) so copied reports match the advertised stage breakdown.
* feat: add Admiral Hardened Build channel and business assurance surfaces
Introduce Studio Saelix entitlement-backed Hardened Build switching, a
single-flight image operation coordinator, Recovery Vault naming, Admiral
Account settings, and typed Fleet update failures while preserving Community
custom-repo and targetless pull-current updates.
* fix: harden image-op paths and clear CI CodeQL/pilot flake
Validate operation IDs before filesystem use, use hostname checks in Fleet
fetch mocks, sanitize registry probe logs, and swallow expected TCP teardown
errors in the pilot reverse-route post-handshake test.
* fix: sanitize image-op docker config write and probe logs
Allowlist-copy registry host keys and base64 auth before writing the
temp DOCKER_CONFIG, and log registry probe failures with a fixed message
so CodeQL no longer flags network-to-file and log-injection mediums.
* fix: address Admiral Hardened Build audit blockers
Expose imageChannel so hardened Fleet peers still POST for typed rejection, claim community updates before 202, terminalize helper failures, gate Hardened on paid, and align support/docs/e2e wording.
* fix: terminalize image ops on helper survival and aborted claims
* fix: prevent recreating persist from overwriting helper-exit failure
* test: assert helper-exit failure lands before recreating persist
* fix: keep current pointer when acknowledging a stale image operation
* feat(resources): show multi-stack usedByStacks on images
Classify images with a deduped sorted stack reverse index, surface chips in the Images table and inspect sheet, and clear node-bound sheet selection on active-node change.
* feat(registries): add exact-ID tag browser with non-401 failures
Add GET /api/registries/:id/tags using credentials for that registry row only, map upstream auth failures to 424, and surface a Registry tags section on the image inspect sheet.
* fix(registries): distinguish unreachable hosts from auth failures
Map auth transport errors to REGISTRY_UPSTREAM (502), surface registry list-load failures in the tag panel, document Used by and Registry tags, and add parser coverage.
* fix(registries): drop unused RegistryTagsPanel __test export
The non-component export tripped react-refresh/only-export-components and failed Frontend lint in CI.
* feat(resources): bind prune to fingerprinted itemized plans
* fix(resources): repair prune plan volume usage and preview list
Source volume RefCount from docker df, keep preview rows from flex-shrinking, tighten managed image attribution and becomesFree, and stop audit summaries from claiming success on rejected prunes.
* feat: add compose discovery for setup preflight and sidebar empty state
Expose read-only compose discovery via GET /api/stacks/discovery and setup
diagnostics. Replace the blank sidebar with path-aware discovery and move
adopt into a dedicated dialog with a three-tab Create Stack flow.
* test: assert post-setup handoff via sessionStorage read-back
The Setup preflight test spied on Storage.prototype.setItem to check the
post-setup adopt handoff. When the jsdom storage probe fails and the test
harness swaps in its in-memory storage stub (which does not extend Storage),
that stub's setItem never touches Storage.prototype, so the spy records zero
calls and the assertion fails even though the component wrote the value.
Read the value back with sessionStorage.getItem instead, matching how every
other storage test in the suite asserts. This is robust to both the native
jsdom storage and the in-memory fallback.
* fix(setup): surface compose discovery as a preflight check row
Drop the Setup discovery banner and non-working Review button. Show
counts as a pass row in EnvironmentChecks (Setup only) and keep
Enter Sencho as the handoff that opens adopt when candidates exist.
* test(setup): cover zero-count discovery row omission
* fix(stacks): widen adopt scan to any yaml and rename into place
Homelab layouts often use nginx.yml or plex.yml. Surface those for
adopt (except overrides), rename to compose.yaml on move so stacks
register, and reset the confirm UI when a move fails.
* feat: add confirmed Take down stack action with optional volume removal
Expose Take down in the stack header and sidebar with a confirmation dialog
that runs compose down while keeping the stack definition on disk. Optional
volume removal is gated by node capability and stack:deploy permission, with
remote gateway preflight before proxying removeVolumes requests.
Closes#1582
* fix: reset take-down volume checkbox when dialog closes
* test: align getStackMenuVisibility assertions with showTakeDown key
getStackMenuVisibility now returns a fifth lifecycle flag, showTakeDown,
but three exhaustive toEqual assertions still listed only the prior four
keys and failed. Add the expected showTakeDown value to each: true for
the partial and exited running-stack cases, false for the self stack.
* test: cover Take down visibility for running non-self stacks
The getStackMenuVisibility assertions exercised the partial and exited
branches and the self-stack guard, but not the raw === 'running' literal
that drives showTakeDown for a normal running stack. Add a case so a
regression dropping 'running' from that check is caught.
* fix: drop Take down from header overflow and wire activity shortcut
Remove duplicate Take down from More actions.
Keep inline button when running, sidebar menu, and Cmd+ArrowDown.
Record stack_taken_down in activity on successful POST /down.
* fix: repin semver compose tags during fleet self-update
Fleet updates failed when docker-compose.yml pinned a semver tag because recreate reused the on-disk pin. Pull the target image first, rewrite semver pins via the update helper, and block digest or unresolved pins with fast 409s.
* fix: update OFFLINE_META shape in capability and node-registry meta tests
* fix: increase masthead rail shimmer visibility
Bumped shimmer highlight opacity from 25% to 50% and reduced
animation duration from 11s to 5s, matching the login page
AuthCanvas shimmer precedent (via-white/60 at 4.5s).
* fix: reduce masthead rail opacity to match login page pattern
Changed all MastheadRail background classes from 100% to 70% opacity
(bg-brand → bg-brand/70, etc.) so the shimmer highlight reads as a surface
reflection rather than a detached floating bar. Matches the AuthCanvas
login card precedent.
* fix: improve masthead rail glow visibility
Replaced the invisible inset box-shadow glow (10px blur inside a 3px
element) with a white overlay that pulses opacity 0.15-0.55 over 4s.
Sped up glow animation from 5.5s to 4s for a more noticeable breath.
* fix: replace cursor-follow tooltip with standard Radix tooltip in sidebar rows
Replaced the Cursor/CursorFollow animate-ui primitives in StackRow
with the standard Radix Tooltip/TooltipTrigger/TooltipContent already
used throughout the app. The custom cursor dot that followed the mouse
is gone; tooltips now appear as static popovers on hover/touch.
* fix: wrap tooltip icon triggers in span for Radix compatibility
TooltipTrigger asChild requires a native element to forward props.
Lucide icon components did not reliably receive data-state and event
handlers, so tooltips on the check-failed and git-pending indicators
did not open. Wrapped each icon in a span.
* fix: rename Notification Routing to Routing in settings sidebar
* fix: replace Timeline/All tasks buttons with SegmentedControl
The two loose Button elements had indistinguishable selected/unselected
states (secondary vs ghost). Replaced with the SegmentedControl component
already used elsewhere on the same page, which has a clear active
highlight indicator.
* fix: standardize Blueprint empty state headings to font-heading
Replaced manual font-serif [font-style:var(--heading-style)] with the
font-heading class on the main heading and three step titles so the
Deployments empty state follows the same heading convention as the
rest of the app and respects the Calm/Signature theme toggle.
* fix: remove custom tooltip styling, use standard TooltipContent defaults
The RowTooltip wrapped its label in a font-mono text-stat-value span
instead of using the TooltipContent base styling (text-xs
text-popover-foreground) like every other tooltip in the app.
* Revert "fix: remove custom tooltip styling, use standard TooltipContent defaults"
This reverts commit 209fbd2742.
* fix: standardize all tooltips to match sidebar RowTooltip design
Wrapped every TooltipContent child in a font-mono text-xs
tabular-nums text-stat-value span so all tooltips across the app
share a consistent look. Previously some used plain text, others
used <p> elements with different fonts and sizes.
* Revert "fix: standardize all tooltips to match sidebar RowTooltip design"
This reverts commit 9e1d97d39e.
* fix: bake standard tooltip styling into TooltipContent component
Added font-mono tabular-nums text-stat-value directly to the
TooltipContent base styles so every tooltip in the app inherits
the same look without per-instance wrappers. Removed the now-
redundant span from StackRow's RowTooltip.
* fix: replace custom tooltip with shadcn/ui Radix tooltip
Replaced the glass/blur-styled tooltip with the standard shadcn/ui
pattern: bg-foreground/text-background, arrow pointer, proper
slide/fade animations, and w-fit max-w-xs sizing. This gives all
tooltips a consistent dark-solid look across the entire app.
* fix: apply glass/blur design to shadcn tooltip component
Replaced the solid bg-foreground/text-background style with the app's
glass design: bg-popover, border-glass-border, backdrop-blur, and
text-popover-foreground. Kept the shadcn structure (Portal, Arrow,
slide/fade animations).
* fix: replace native title tooltips with Radix tooltips on Fleet toolbar
Converted Refresh, Export Dossier, Search, Sort direction, Node Update,
and Manage Nodes buttons from native HTML title attributes to the shared
Radix Tooltip component so they match the app-wide glass tooltip design.
* fix: replace native title tooltip on Fleet Snapshots upload button
* fix: replace native title tooltips with Radix tooltips on Resources page
Converted all actionable button tooltips (Open stack, Inspect image,
Scan, Browse volume, and delete buttons) plus the SenchoBadge
informational tooltip. Wrapped disabled protected-resource delete
buttons in span triggers so tooltips fire on disabled elements.
* fix: replace native title tooltips with Radix tooltips on Security page
Converted scanner info icon, exploitability n/a badge, and scan
image button from native HTML title attributes to the shared Radix
Tooltip component.
* fix: replace native title tooltips with Radix tooltips on Schedules and Settings pages
Converted Run now, Execution history, Edit, Delete buttons on Schedules;
Download/Delete on Cloud Backup; Send test/Edit/Delete on Notification
Routing; Reset 2FA on Users; and Edit/Remove suppression on
Suppressions panel. Skipped SettingsSection/Modal title props which
are React component props, not native HTML attributes.
* feat: add copy button to execution history details column
Replaced the plain truncated Details cell with a flex layout containing
a copy button. Users can now copy the full error/output text instead
of relying on the native title tooltip to read long messages.
* fix: change tab highlight from neutral to brand color
Replaced bg-glass-highlight with bg-brand/20 on all TabsHighlight
instances and updated TabsTrigger active state to text-brand,
matching the SegmentedControl selected style. Works on both
dark and light themes.
* fix: restructure Resources page tabs to match Fleet/Security pattern
Moved tab band out of the outer card into a standalone full-width
element. Replaced custom FilterToggle with Fleet-style Button
toggles. Added search inputs per tab. Wrapped each resource table
in a card container matching the Security Images table design.
* fix: match Resources tab font size to Fleet and Security
Removed explicit text-xs from Resources TabsTrigger elements so they
use the default text-sm, matching Fleet and Security tab typography.
* fix: restore missing closing bracket on TabsTrigger tag
* fix: add aria-labels to schedule action buttons, update test selectors
Replaced native title attributes with aria-labels on Run now,
Execution history, Edit, and Delete buttons so the Radix tooltip
conversion does not break test selectors that relied on title.
Updated tests from findByTitle to findByRole.
* fix: add fingerprint-based dismiss to post-deploy scan banner
Created useScanBannerDismiss hook following the usePreflightDismiss
pattern. The banner now persists dismissal in localStorage keyed to a
fingerprint of scan status + attemptedAt. Dismissal survives page
reloads for the same scan outcome, and the banner automatically
reappears when a new scan runs or the status changes.
* fix: update Configuration Status card labels and add new rows
Notifications section: renamed Notification agents to Channels,
Notification routing to Routing, added Mute Rules row linking to
notification-suppression settings.
Security section: added Trivy installed Yes/No row, renamed
Vulnerability scanning to Scan policies.
Backend: added trivyInstalled and suppressionRules fields to
the /dashboard/configuration payload.
* fix: style Trivy row as badge and link to Security scanner setup
* fix: remove Mute button from stack anatomy panel header
* Revert "fix: remove Mute button from stack anatomy panel header"
This reverts commit 16843a81b5.
* fix: remove Mute button from stack anatomy panel header
* fix: remove unused stackMuteActions imports and props from StackAnatomyPanel
The removal of the ActivityMuteKebab rendering left orphaned imports
and props that cause TS6133 errors under strict mode. Clean up the
imports, prop type, destructuring, and caller prop passes.
* fix: refine anatomy tab sizing and add pulsing scroll chevron
Changed anatomy tabs from 12px fixed height to 11px with py-1
padding, matching the Fleet tab approach. Bumped Files/Edit buttons
from 10px to xs for better visibility. Added subtle animate-pulse
to the right scroll chevron to hint at overflow tabs, gated behind
the Reduced effects appearance setting.
* fix: link reclaim hero number to heading style
* fix: replace native title tooltips with Radix tooltips on stack detail page
Converted Expand/Collapse logs, Stats unavailable, Copy service URL,
Dismiss findings, Likely secret badge, Move up/down, and Remove
buttons from native HTML title attributes to the shared Radix
glass tooltip component.
* fix: replace native title tooltip on image source link button
* fix: add tooltips to container logs and bash session buttons
* fix: add tooltip to download logs button
* fix: add tooltip to copy digest button
* fix: replace cursor-follow tooltip with Radix tooltip on severity badge
* fix: add tooltips to density toggle buttons
* feat: add expand button to multi-container health panel
Added an expand/collapse toggle next to the density controls that
expands the containers panel to fill the column and hides the logs
section. The expand and logs-expand are mutually exclusive. Only
appears for stacks with more than one container.
* fix: reduce Structured/Raw terminal toggle font to 10px
* fix: set compact density as default for multi-container stacks
* fix: change Feedback to Open New Issue with GitHub issues link
* fix: replace ExternalLink with GitHub icon for Open New Issue menu item
* fix: restore ExternalLink import needed by other menu items
* fix: default host threshold alerts to off for new installations
* fix: resolve CI typecheck and test failures
Broaden MenuRowProps.icon type to accept custom SVG components alongside
lucide icons, fixing TS2741 on GitHubIcon import.
Update host_alerts_enabled seed test assertion to match the new opt-in
default of off (0).
* fix: update ContainersHealth tests for compact default density
Adjust three tests to reflect the new default of compact mode:
- Rename 'detailed mode is the default' to 'compact mode is the default'
- Update sparkline visibility checks to switch to detailed first
- Rename remount test and verify reset to compact (new default)
* feat: block self-stack lifecycle ops with UI and preflight guardrails
Refuse update, deploy, down, stop, and delete when the stack matches Sencho's compose project.
Return 409 self_stack_protected. Expose isSelf on /statuses and disable guarded UI actions.
Add SelfStackProtectedDialog and self-managed-stack preflight warning.
Closes#1564
* fix: add missing stackSelfFlags mock to useSidebarContextMenu test
The production hook now reads stackListState.stackSelfFlags[file], but the
test mock did not include it, causing 6 tests to fail with TypeError:
Cannot read properties of undefined (reading 'web.yml').
* fix: harden self-stack protection during startup
Add a global environment preflight warning when Sencho is managed inside COMPOSE_DIR.
Align status decoration and route guards on Docker label fallback detection.
Block rollback and service-level stop on the protected self stack.
* fix: add self_stack_location to diagnostics-route expected check IDs
Only one local node is allowed. Creating a second local returns 409,
and the last local node cannot be deleted or converted to a remote type.
Existing duplicate local nodes from older versions are preserved and can
be cleaned up individually. Zero-local recovery auto-assigns the default
flag. Frontend delete surfaces and the Add Node form respect the new
invariant.
Enforced in DatabaseService (addNode/updateNode/deleteNode guards) and
routes (error translations). Legacy test fixtures use raw SQL helpers.
* feat: acknowledge Compose Doctor preflight findings per stack
Add node-scoped preflight acknowledgements with read-time filtering.
Supports four expiry modes and activeStatus for banner, tab dot, and readiness.
* fix: align preflight acknowledge UI with design system
Use Combobox, modal chrome, mono fields, and non-destructive clear confirm.
* fix: update test mocks to match new preflight field names
The preflight-acknowledgements feature renamed status-\>activeStatus and
highestSeverity-\>activeHighestSeverity in the preflight report shape. The
corresponding test mocks in three files still used the old field names,
causing 6 test failures across backend and frontend.
- backend: update-guard-service mock now passes activeStatus
- frontend PreflightPanel: Report interface and report() helper now include
activeStatus, activeHighestSeverity, activeCount, acknowledgedCount
- frontend StackAnatomyPanel doctor: mock API response now includes
activeHighestSeverity and activeStatus
* feat: add sidebar update indicator toggle and Stack Health badge
- Add image_update_sidebar_indicators setting (default off, node-scoped)
- Gate the Updates filter chip and sidebar status indicators on the setting
- Add "Update available" badge to Stack Health table (always visible)
- Extend ImageUpdateStatus with sidebarIndicators boolean
- Poll /api/image-updates/status alongside /detail in useImageUpdates
- React to SENCHO_SETTINGS_CHANGED for instant toggle propagation
- Reset sidebar state on node switch; generation-guard stale responses
- Disable toggle when status is null (loading) or field is absent (old node)
- Wire stackUpdates through ViewRouter → HomeDashboard → StackHealthTable
- Update settings registry, operator docs, and sidebar/dashboard docs
* fix: guard against stale node renders, memo drift, and cross-node error toasts
- Track owning node ID in useImageUpdates state so React never renders
node B with node A's data before the passive effect resets (P2)
- Replace incorrect stackUpdates dependency with sidebarStackUpdates in
chipFilteredFiles useMemo (P3)
- Guard the error toast in handleSidebarIndicatorsChange so a stale PATCH
failure from node A does not surface while viewing node B (P3)
* fix: default sidebar update indicators to on (opt-out)
The sidebar indicators are a safe convenience that most users want.
Switching the default from off to on matches the opt-out convention
used by prune_on_update, reclaim_hero, and health_gate_enabled.
* feat: move core Blueprint orchestration to Community tier
Blueprints CRUD, reconciliation, and drift modes are now available on
Community. Pin remains Admiral-only via Federation placement controls.
* feat: move Federation placement controls to Community tier
Remove requirePaid from cordon, uncordon, and blueprint pin routes. Ungate the Federation tab and gate cordon UI on node:manage only. Update licensing and fleet docs for the new tier split.
* feat(scheduler): schedule container restart, stop, and start
Add container as a scheduled-task target type so operators can automate lifecycle actions against standalone containers by node and name, with matching UI pickers, validation, execution on local and remote nodes, and tests.
* fix(scheduler): stack service matching and container picker hygiene
Backfill Service on smartFallback containers so per-service stack restarts work when container_name is set. Match services by compose label and container name in stack routes and scheduled restarts. Exclude Sencho from GET /api/containers lists. Hide the Restart Stack service picker when a stack has only one service.
* test(scheduler): scope service checkbox assertion to Services block
The create dialog also has a Delete after run checkbox. Count checkboxes only inside the Services section so CI does not include unrelated form controls.
* fix(scheduler): narrow closest() result to HTMLElement in schedule test
The service-checkbox assertion passed an Element from closest() into
within(), which requires an HTMLElement, failing tsc -b in the frontend
build and Docker build stages. Use the closest<HTMLElement>() type
argument so the value type-checks without an unsafe cast.
* fix(scheduler): hide Sencho container on remote node picker lists
Remote container lists are proxied from peer Sencho instances, so id-only self filtering missed peers on older builds. Await SelfIdentity init, match ImageID, and drop official saelix/sencho images. Apply the same heuristic in the scheduled-operations UI and when the hub fetches remote containers for scheduled runs.
* test(monitor): add missing DatabaseService mocks for scan history cleanup
* test(scheduler): add missing markStaleScansAsFailed mock
SchedulerService.tick() calls db.markStaleScansAsFailed() to sweep stale
vulnerability scans. The scheduler-service test was missing this method in
its DatabaseService mock, causing TypeError failures during test initialization.
Added mockMarkStaleScansAsFailed to hoisted mocks and DatabaseService mock
object, returning safe default of 0 scans marked as failed.
* test(compose): add missing FileSystemService mocks for getStackContent/getEnvContent
* test(containers-route): mock SelfIdentityService to prevent initialize() crash
The excludeSelfContainers() helper calls SelfIdentityService.initialize(), which tries to access DockerController. Without a proper SelfIdentityService mock, the initialize() call fails silently, causing a 500 error on GET /api/containers.
Added SelfIdentityService mock with initialize(), isOwnContainer(), and isOwnImage() methods to prevent the crash.
* feat: add notification suppression rules
* fix: restore label routing and routing test mocks for suppression
* fix: allow bell mute shortcuts for history-only notification categories
Suppression rule validation used the routable category whitelist, which rejected history-only categories such as update_started that appear in the bell during stack updates.
* feat: expand Mute Rules UX with compose-first entry points and activity badges
* fix: add missing NodeContext mocks for notification suppression tests
* fix: enforce 1:1 compose path mapping for Pilot agent mounts
Pilot enrollment now generates validated 1:1 bind mounts so every
agent path maps to a unique compose directory. Persisted agent paths
reconcile during startup to catch drift. Unsafe relative-bind redeploys
are blocked before container removal to prevent path escapes.
- Add composePathMapping utility with strict path validation
- Generate COMPOSE_DIR and validated mounts during Pilot enrollment
- Reconcile persisted agent paths during startup bootstrap
- Block redeploy when a relative-bind mount would escape the compose root
- Default Pilot UI path to /opt/docker/sencho
- Update multi-node and pilot-agent documentation
- Add regression tests for enrollment, bootstrap, compose-service,
and environment-check paths
* fix: update E2E enrollment regexes for YAML-quoted token values
The cross-node capability gate cached its verdict, so a remote replaced by older
code at the same URL stayed trusted until the cache expired, reopening the
non-admin HTTP escalation and the over-broad stop. The probe now hits the
remote's live /api/meta on every gated action (concurrent calls deduped, never
cached across requests, fail-closed), so a downgraded remote is detected
immediately.
Two stop-result gaps are also closed:
- A remote stop result must now cover exactly the confirmed stacks (one per
stack, no extras, no omissions), not merely exclude extras, so a dropped
confirmed stack is no longer accepted as clean. runLocalLabelStop reports one
result per confirmed stack even when the label has vanished, so a current
remote always satisfies the check.
- The local stop exception path now reports the full confirmed set, so a
confirmed stack that lost its label is not dropped when the local stop throws.
An older remote node ignores the forwarded actor-role header (running proxied
requests as admin) and ignores the stop-by-label stack allowlist (stopping
every label-matched stack). The control could neither detect nor prevent this
on a mixed-version fleet.
Instances now advertise a cross-node-rbac capability, and the control refuses
to act when a remote lacks it:
- HTTP proxy: a non-admin user's request is not forwarded to a remote that does
not advertise the capability (fails closed when it cannot be determined).
Admins are unaffected.
- Stop-by-label: a real stop bound to a confirmed stack set is not sent to a
remote lacking the capability; the node is reported as needing an upgrade. As
defense in depth, a node whose results name stacks outside the confirmed set
is failed rather than rendered as a clean stop.
Separately, the stop's lock-contention path now reports every confirmed stack
as a contention failure (including one that lost its label), so a confirmed
stack is never silently dropped and the result is never empty.
The fleet overview, configuration, dependency-map, and networking-summary reads
were authentication-only, so a role without node:read (deployer) could read node
names, host stats, and cross-node topology. They now require node:read, matching
the role model where every role except deployer holds it.
For parity, the Fleet nav entry is gated on node:read (hiding it from the top
nav, mobile menu, and command palette), the Fleet view redirects to the
dashboard when reached without it, and the dashboard fleet heartbeat falls back
to the single-node restart map for a role that cannot read fleet data.
A fleet stop re-matched stacks by label name at execution, so a stack that
gained the label between the operator's preview and confirmation could be
stopped even though it never appeared in the confirmation. A confirmed node
that was deleted after the preview also vanished from the results, letting the
remaining successes read as a clean stop.
The confirm flow now sends the exact node and stack list resolved in the
preview. Each node's stop is bound to that set: only stacks that are still
label-matched and confirmed are stopped, and a confirmed node missing from the
registry is reported as an explicit failure rather than dropped.
Scan policies, deploy enforcement, the suppression-aware deploy-block
toggle, SARIF export, and OpenVEX export now work on Community, matching
the rest of the vulnerability-scanning surface that was already free.
Backend: drop the tier gate from the seven security routes and from the
dashboard configuration-status scan-policies row, so the Dashboard and
Fleet config cards stop hiding the Vulnerability scanning row. Reading
policies stays auth-only; mutations and exports stay admin-only.
Frontend: always show the Policies tab and panel, the SARIF and VEX
export actions, and the honor-suppressions toggle for admins.
Docs: move scan policies, SARIF, and OpenVEX to every tier across the
feature and API-reference pages; clarify that Fleet Sync's cross-node
replication remains the paid part.
* fix: harden cross-node fleet label actions and guard container reads
Release-stabilization fixes for the Fleet Actions surface:
- Stop-by-label binds execution to the nodes shown in the confirmed
preview. The real stop sends the confirmed node ids and the backend
restricts the fan-out to them, so a node that was unreachable during
preview and reconnects before the stop can no longer enter execution
and have unlisted stacks stopped.
- Bulk label assign validates each remote node's result against the
stacks it was asked to label: a body whose results are empty, partial,
duplicated, or shaped wrong is a per-node failure instead of reading as
a successful zero-stack assign. The card mirrors this, rejecting a
missing or non-array results body and only reporting success when at
least one stack was assigned.
- Bulk label assign re-reads authoritative per-node stacks and labels on
demand via a Refresh control, and the confirmation lists the affected
node and stack names rather than bare counts.
- The stack-specific and fleet container/stack read routes require the
stack:read permission, matching the generic container and stack routes.
Every shipped role already carries stack:read, so reachability is
unchanged; the guard closes the routes that were auth-only.
Adds unit coverage for the assign-result validator, route coverage for
the stop allowlist and assign membership checks, and authorization
coverage for the newly guarded reads.
* test: assert the confirmed node allowlist in the fleet stop-card test
The stop-card component test pinned the real-stop request body to
{ labelName, dryRun } and broke once the stop began carrying the
confirmed-preview node ids. Update it to expect the nodeIds allowlist
derived from the resolved preview, so the test asserts the binding
rather than the pre-fix shape.
PR #1497 pinned an enabled one-shot's next_run_at on creation, but the chosen
instant did not survive two paths: editing reconstructed the date from the
yearless cron (current year), and a disabled one-shot nulled next_run_at with no
other store, so enabling it later recomputed from the cron. Both moved a
future-year one-shot to a different annual occurrence than the date displayed.
Persist the one-shot's absolute fire time in a dedicated run_at column
(additive, nullable; recurring schedules leave it null). Create and update store
run_at independently of the enabled state; next_run_at is derived from it when
enabled and null while disabled, so a disabled one-shot keeps its run_at and the
enable toggle restores the exact instant from the column rather than the cron.
The editor reconstructs a one-shot's date from the persisted run_at, so opening
and re-saving without changes preserves the originally chosen year.
No behavior change for recurring schedules or fresh installs; the column is added
by an additive migration safe for upgrades from v0.92.0.
Make the Resources Images, Volumes, and Networks tables sortable with a
shared useTableSort hook and SortableTableHead, move the Images scan-history
control into the Images tab header, and keep the network List/Topology toggle
anchored with Create Network visible in both modes.
Rework the dashboard Stack health table: drop the redundant Host column, add
sortable Stack/Up/CPU/Mem headers, and add Source (local/git) and Port columns.
The status endpoint now labels each stack with its git/local source, computed
outside the cache so linking changes show immediately.
Extract a reusable CreateNetworkDialog and add a create-network action to the
stack-detail Networking tab.
Simple "Once" schedules compiled to a 5-field cron, which has no year field, so
the scheduler computed the next run as the next annual occurrence. A date chosen
for a later year ran a year early, and a time already elapsed today ran a year
late, contradicting the UI promise that the task fires on the chosen date.
One-time schedules now send the chosen absolute timestamp (run_at) and the
backend pins next_run_at to it instead of the cron-derived next run, so the run
fires on the exact selected instant including the year. The Simple-mode
validation now compares the full chosen instant against the current time, so a
time earlier today is rejected as past rather than silently deferred a year.
run_at is validated as a finite, future epoch-millisecond timestamp on create
and update. The enable/disable toggle preserves a one-shot's pinned next_run_at
(its yearless cron cannot reconstruct the chosen year), so re-enabling restores
the exact instant. Recurring shapes and Advanced mode are unchanged (cron stays
authoritative).
The changelog tab fetched release notes once and held them in component state
without tying them to a version, and the endpoint did not report which release
the notes belonged to. When a newer release surfaced while the sheet stayed
mounted, reopening the changelog could show the previous version's notes, and a
GitHub/Docker Hub fallback or independent cache timing could leave the notes out
of sync with the advertised latest version.
The release-notes endpoint now returns the release version (normalized
tag_name). The changelog keys its loaded notes to the advertised latest version,
refetching when that version changes, and labels the notes with the version they
belong to so the displayed content is always explicit.
The scan-detail banner only recomputed its verdict when honor-suppressions was
enabled; otherwise it returned the snapshot stored at scan time. The deploy gate
always re-evaluates current policies, so with honor-suppressions off a policy
lifecycle change drifted the banner from the gate: disabling (or editing) an
enabled policy left the banner claiming a violation the gate would now pass, and
tightening a passing policy left the banner reporting a pass the gate would block.
Recompute the banner verdict unconditionally so it agrees with the gate across
the full policy and suppression lifecycle, regardless of the honor-suppressions
setting. The recompute already reads that setting itself (so a raw or
suppression-filtered verdict is chosen correctly), returns no verdict when no
policy matches (clearing the banner to match a passing gate), stays read-only,
and still falls back to the stored snapshot if it throws.
The policy verdict shown on the scan-detail banner was the snapshot computed
once at scan time. When the deploy gate is set to honor suppressions, the gate
re-reads current suppressions on every deploy, but the stored verdict never
changed: after creating a matching suppression the banner kept claiming a
violation the gate would now pass, and after deleting or expiring a suppression
the banner kept reporting "ok" while the gate would block.
Recompute the banner verdict against current suppressions when serving the scan
detail, so it agrees with the gate across the create, update, delete, and expire
lifecycle. The recompute runs only when honor-suppressions is enabled (otherwise
suppressions affect neither the gate nor the verdict, and the stored snapshot is
returned unchanged), is read-only, and falls back to the stored snapshot if it
fails so an informational banner can never fail the scan-detail request.
The Security overview and exploit-intel surfaces picked the latest scan per
image without restricting to scans that ran the vulnerability scanner, and
counted known-exploited (KEV) findings only among Critical/High. Two effects:
- A newer secret-only node scan became the latest scan for an image and
clobbered its Critical/High/fixable/KEV posture to zero, which could read a
false Secure state.
- A Medium or Low severity KEV that the pre-deploy gate blocks on produced zero
overview and exploit-intel rows, so the page disagreed with the gate.
Posture queries now select the latest vulnerability-bearing scan per image, the
image summary sources its vulnerability counts from that scan via a LEFT JOIN
while still counting secret and misconfiguration findings from the latest scan
overall, and knownExploited is counted from a dedicated any-severity KEV query
that mirrors the gate.
* fix: distinguish failed image-update checks from "up to date"
The image-update detector collapsed every failure (registry unreachable,
missing auth, rate limit, unresolved local digest) into hasUpdate:false and
dropped the captured reason, so a failed check was indistinguishable from a
current image and never raised a notification, even while a manual stack
update still pulled a newer image.
Detection now records a tri-state per stack (ok / partial / failed) with the
failure reason, exposed via a new GET /api/image-updates/detail (the boolean
GET / is unchanged so fleet aggregation is unaffected). A fully-failed check
preserves the last known has_update, so a transient outage neither erases a
real update nor flaps the notification state. The sidebar shows a muted
"couldn't check" indicator with the reason on hover, and the Update board
lists stacks whose check failed in a "could not be checked" advisory.
Detector hardening: the manifest digest lookup issues HEAD first (falling back
to GET) so it no longer draws down Docker Hub's anonymous pull-rate budget, and
local RepoDigest matching is normalized so official library/* images resolve
their digest instead of falling through to a silent "no update".
* fix: preserve confirmed updates through partial checks; tighten failure surfacing
Address review findings on the tri-state image-update detection:
- A partial check (some images errored) no longer erases a previously
confirmed update; only a fully-ok check can lower has_update, so a single
image's registry blip cannot drop the stack's update and re-fire the
notification on recovery. Adds a regression test.
- The image-level catch stores getErrorMessage(e) rather than raw String(e),
since that value surfaces verbatim in the sidebar tooltip and readiness
advisory.
- useImageUpdates and the readiness detail fetch now log unexpected non-ok
responses instead of silently leaving stale state.
- Remove an unused checkFailedCount derivation (the row indicator is driven by
the checkStatus prop).
- Reword the recordStackCheckFailure docstring and the HEAD-first comment.
The auto-update, bulk-label, scheduler, and blueprint deploy block
messages hardcoded "image(s) exceed <max_severity>", which is wrong
under the risk-first policy model: a block can be driven by a
known-exploited (KEV) or fixable Critical/High input while the severity
threshold was never the trigger. In those cases the message named a
severity ceiling the policy did not enforce.
Route all four message paths through a shared summarizeBlockReasons
helper (the same reason text the deploy-gate 409 response and the block
dialog already use), so every surface names the inputs that actually
matched. Falls back to a generic phrase when no reason was recorded.
Vulnerability scan rows were never cleaned up when their image was removed
from Docker or their stack was deleted, so the Security Overview (including
the Top exploit-risk findings card) kept surfacing findings for artifacts that
no longer exist.
Scan results now reflect what is still on the host:
- Deleting a stack immediately purges its stack:<name> compose-config scan.
- A background reconciliation in the monitor janitor removes scans whose image
is gone from the node, or whose stack folder no longer exists. It is
fail-safe: a scan is only removed when its artifact is positively known to be
gone, the Docker image list is read with a timeout (skipped on failure), and
stack scans are reconciled only when the stack list is non-empty.
- An opt-out "Remove scans for deleted images and stacks" setting (on by
default, per-node) lets operators retain scan history for removed artifacts.
Scan deletes remove child findings explicitly, since SQLite foreign-key cascade
is not enabled on the connection.
* feat: add node update alerts with changelog tab and skip-version handling
- Add node_update_available notification category with blue/brand bell dot
- Route node_update_available notifications to Fleet -> Node updates sheet
- Add Changelog tab to NodeUpdatesSheet with GitHub release notes
- Add per-node skip-version persistence (node_update_skips table)
- Skip hides update CTA on node card and sheet; re-surfaces on newer version
- Skipped nodes excluded from Update all backend filter
- Add pulsating dot indicator on Changelog tab when updates available
- Always-visible View changelog action in notification row bottom
- Admin-only for all mutating controls (skip, unskip, update)
- Backend tests for skip-version semantics (15 tests)
- Update fleet-view.mdx, remote-updates.mdx, and OpenAPI spec
* fix: address audit findings - nested button, stale changelog, semver normalization, mobile intent
- Move View changelog button outside routable button (sibling element)
- Fix aria-label for node_update_available notification rows
- Support ?recheck=true on release-notes endpoint
- Invalidate release notes cache on forced recheck
- Store normalized semver (semver.valid strips v prefix)
- Skip fleetUpdatesIntent on mobile (desktop only)
- Add v-prefix normalization test
* fix: restore View changelog on same line as timestamp, opposite sides
The button is always visible at the bottom right of the notification card,
on the same row as the timestamp (just now), using justify-between layout.
* fix: update tests for node_update_available category and release-notes fetch
- Backend: monitor-service tests now expect node_update_available instead of system
- Frontend: NodeUpdatesSheet tests mock release-notes API call to prevent undefined then()
* fix: resolve ci lint failures