* 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(ui): add Classic, Smart, and Compact desktop navigation styles
Introduce a shared app-nav registry and reachable model so TopBar, the
command palette, and mobile menus share one destination source. Smart bar
is the default; Appearance gains a Navigation subsection with quick links.
* fix(e2e): stop clearing top-nav prefs on every reload
The desktop-navigation suite used addInitScript to wipe mode storage,
which re-ran on reload and undid the classic/compact values under test.
* fix(ui): harden nav PR docs and Compact quick-link coverage
Drop the partial docs-refresh import that left missing image assets and a stale Display screenshot, keep navigation-scoped operator docs against main, and add an E2E path that proves Compact quick-link add, persist, and render after reload.
* fix(ui): polish Compact quick links and menu mastheads
Align Smart/Compact menus with Theme chrome, keep pin labels always visible, and drive add capacity from persisted pins (max five) with a trailing + picker and per-pin remove.
* test(e2e): exact-match Compact Networking pin locator
Avoid Playwright strict-mode clash with Actions for Networking.
* fix(ui): simplify Compact quick link removal and fix + button trailing
Remove the (...) dropdown per quick link in Compact mode. Right-click
context menu remains as the sole on-bar removal affordance. Fix the +
add-button to trail quick links rather than pinning to the far right
by removing flex-1 from the quick-link rail.
* fix(notifications): neutralize satellite-local node names in alert bodies
Fleet-aggregated alerts embedded each instance seed name (often Local)
while the hub badge already named the remote. Drop identity prefixes and
use type-aware local wording so attribution stays on the badge.
* fix(docs): correct image-update default check cadence
Operator docs still said six-hour polling; the seeded default is two
hours in interval mode, and the cadence is configurable or cron-based.
* test(notifications): assert hub stamps roster name on neutral remote bodies
Cover the fan-in path that attaches hub roster identity while leaving the
satellite message body unchanged.
* 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.
Reframe docs, OpenAPI registries copy, and License trial/expiry helpers
so Community is the full AGPLv3 product and Admiral leads with current
assurance (Hardened Build, Recovery Vault, support, governance).
Mesh, Secrets, and Host Console stay limited-availability without
Admiral sales framing.
Fleet-aggregated reclaim alerts showed Node Local in the body while the
badge already named the remote. Use a node-neutral message so identity
comes from the hub badge; leave stored rows unchanged.
* fix(ui): default Reduced Motion on Calm and quiet decorative rails
Calm (and Reset to default) enables Reduced Motion; Signature clears it. Manual Motion stays independent of the visual-style card. Reduced Effects also stops decorative masthead rail animations. Related to #1614; does not close that issue.
* test(e2e): fix Calm Motion specs racing empty-create compose edit
Stop appearance init-script reseeding after mid-test mutations, and wait for Save and Deploy / mobile compose editor after empty create instead of assuming Anatomy Edit compose.
Remove the MonitorService 6-hour version-check cooldown so node_update_available
fires when the shared version cache observes a published release, matching the
Fleet update button. Dedup and publish-pending gating still prevent spam.
* 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
* docs: align README and overview docs with current AGPLv3 tier model
Community is now described as the complete free product, with a single
consolidated Admiral section replacing scattered per-bullet tags that had
drifted out of sync with the actual gates (security scanning, private
registries, and most of RBAC/audit moved to Community over time). Remove
promotion of Sencho Mesh, Fleet Secrets, and Host Console, which are no
longer discoverable by default. Align the Fleet Secrets, Host Console, and
Sencho Mesh sections in the features overview with the limited-availability
framing already used elsewhere on that page.
Refresh every README screenshot to the current UI and add new captures for
Security, Blueprints, Scheduled Operations, and Compose Doctor.
The introductory tier-framing sentence and docs navigation placement for the
now-hidden surfaces are intentionally left for a later, dedicated pass once
the Admiral positioning work settles.
* docs: restructure README around the open-source Community product
Drop "What Sencho is not (yet)", fold its known-limitations pointer into
the Documentation section. Rewrite Telemetry and data handling to drop the
Admiral license-validation detail, since this file is the open-source
entry point and Admiral customers work from their own agreements. Move
the Admiral section to just before Documentation, community, and license
so the README leads with the Community product throughout.
Gate Routing, Secrets, Host Console, and Mesh dashboard/settings surfaces on the existing useExperimental readiness flag so immature operator surfaces stay out of the default UI while paid and admin backend gates remain unchanged.
* docs: relicense Sencho to AGPLv3 and reframe Community positioning
Replace BSL with AGPLv3 for the public Community product, update contributor
and licensing copy for Community-focused contributions, and surface source
and license links in Settings About.
* docs: clarify CLA scope and Community contribution framing
* fix(ui): split About link constants and harden AboutSection test selectors
* fix(ui): lower-overhead Reduced effects path for constrained GPUs
Disable backdrop-filter glass and solidify chrome fills when data-effects is reduced (Calm default), with Appearance warning and docs for constrained rendering devices. Related to #1614.
* test(ui): harden Reduced effects glass e2e and docs wording
Parse Color Level 4 slash alpha so opacity asserts are not vacuously true, cover dialog/toast/overlay both ways with stable data-sn hooks, and soften troubleshooting copy that overstated the unmeasured diagnosis.
* docs(ui): lead constrained-GPU guidance with Reduced Motion
Rename the troubleshooting section to neutral GPU framing, point Appearance and docs at Reduced Motion first, and show the callout whenever Motion is off so Calm or Reduced effects alone do not hide the validated workaround.
The Timeline lane marker lacked shrink-0, so inside the fixed 80px label
column the widest labels flex-shrank their color dot to zero width. The
Maintenance and Lifecycle dots collapsed and rendered as no dot at all,
while the shorter labels kept theirs. Add shrink-0 so every lane dot
renders at a consistent size.
Maintenance was the only label wide enough to fill the 80px column on its
own, which is what left its dot no room. Rename it to Upkeep so all five
labels fit and align. A registry test now pins each lane's key, order,
and label, and asserts every lane carries a color, so a future rewrite of
the action registry cannot silently rename a lane or drop a dot color.
* feat(security): add triage status and OpenVEX justification parity
Share triage options across SuppressionsPanel and the scan-sheet suppress dialog, require justification for not_affected and false_positive, and document the fields.
* fix(security): use design-system Select for triage dropdowns
Replace native selects so OpenVEX justification options use themed popover content instead of unreadable OS option lists in dark mode.
* fix(security): keep triage justification Select controlled
Pass an empty string instead of undefined so Radix Select does not flip between uncontrolled and controlled when the placeholder is shown.
* 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.
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(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.
Non-admin users no longer see Update buttons in Fleet View; the UI shows
read-only "Available" badges instead. The accordion describing a 403-click
path was orphaned by the frontend role-gating that shipped in an earlier PR.
The Note above the troubleshooting section already states the admin-role
requirement in plain language.
* feat: open compose editor in edit mode with one click
Remove the desktop double edit gate so Anatomy "Edit compose" and empty-stack
create land in an immediately editable workspace. Close discards both compose
and env buffers; deferred loads keep startInComposeEdit through unsaved confirm.
* docs: align compose editor close behavior and Edit compose labels
Update stack-management and editor docs for one-click edit mode.
Close discards unsaved compose/env edits. Anatomy shortcuts use Edit compose.
* fix(e2e): update routing test to use anatomy-edit-compose-btn test id
The compose editor env tab routing test was written against the old
'edit' button name. The single-compose-edit-gate branch renamed that
button to 'Edit compose' with test id 'anatomy-edit-compose-btn'.
Update the selector to match.
* 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.
Sidebar opens /stacks/:name (anatomy). Monaco uses /compose|/env|/files.
Refresh of a detail URL no longer opens the editor, and editor deep links
keep a hydration shell instead of flashing the dashboard.
* fix(routing): basename env URLs and defer writes during node hydration
Encode only env file basenames in ?env= and omit the default file.
Resolve legacy absolute-path bookmarks on load.
Block history writes until the active node matches a cold-loaded remote deep link.
* fix(routing): restore env deep links after stack load hydrates file list
Defer env selection until envFiles is populated after loadFileForRoute.
Apply default env when the URL omits ?env= (Back/popstate).
Document env URL rules and legacy basename resolution in deep-links.mdx.
* feat: add routable browser URLs for stacks and shell views
Sync in-memory navigation to the address bar via a History API hook so
deep links, refresh, Back/Forward, and bookmarks work across nodes,
views, stack editor tabs, and mobile surfaces. Gate role/tier URL
normalization on permissions and license readiness, preserve URLs on
metadata fetch failure, and surface retryable stack-list errors without
rewriting pending stack paths.
* fix: preserve deep-link views on cold load and refresh
Stop the node-switch effect from resetting to dashboard on initial mount.
Defer URL writer settlement until hydrated activeView matches the route.
Adds E2E coverage for shell cold loads, stack refresh, and compose env tab.
* fix: keep mobile dashboard on list surface so sidebar renders
On mobile, the URL sync hook was routing /nodes/<slug>/dashboard to the
content surface, hiding the stack list sidebar. This prevented the
data-stacks-loaded sentinel from appearing, causing sidebar truncation
E2E tests to time out after reload on a mobile viewport.
Mobile dashboard now stays on the list surface; other non-editor views
still render on the content surface.
* fix: complete mobile URL routing follow-ups for stack deep links
Restore mobile /dashboard vs /stacks, list surface always writes /stacks.
Hydrate pendingDetailStack, freeze compose failures with routeDetailError,
and add unit plus E2E coverage.
* fix: hydrate shell views from URL and sync in-app navigation
Bootstrap activeView and tab state from the pathname on cold load.
Settle route phase when state already matches, normalize unknown segments,
and open Monaco editor tabs from stack deep links via applyEditorRouteState.
* fix: prevent mobile stack deep links from hanging on cold load
The resolvePendingStack effect did not re-fire when the pending stack ref
was populated during URL hydration, because the urlHydratingStack state
set in the same callback was not listed in the effect's dependency array.
Adding it causes the effect to retry once hydration has committed.
A resolvingRef mutex prevents concurrent invocations. When the target file
is already loaded, route state is applied directly without calling
loadFileForRoute, which avoids unmounting the editor (and hiding the
recovery chip) if a background refresh triggers route resolution during
a deploy operation.
* test: adapt stack, deploy, and sidebar e2e specs to routable stack URLs
* ci: raise E2E Playwright job timeout to 20 minutes
* 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
Closes#1580. The Command Center header showed the first container's
image directly above stack-wide Start/Stop/Update controls, which implied
those buttons targeted one image. Remove the header image/digest row;
per-container ImageSourceMenu on each row remains.
* 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
Drift compared raw compose YAML to runtime, so ${VAR:-default} image
expressions false-positive as image-mismatch. Source the declared side
from docker compose config instead, matching deploy-time resolution.
Fixes#1572
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.
Detect services with build: in the update preview and run compose build --pull
plus pull --ignore-buildable when Update is triggered on those stacks, while
keeping the existing pull-only path for image-only stacks.
* 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: bound container health pane so logs stay visible on large stacks
On the stack overview page, multi-container stacks (8+ services) pushed the
logs pane off-screen because the Command Center card had shrink-0 and no
scroll mechanism. This reworks the desktop layout for stacks with more than
one container:
- The Command Center card is bounded at max-h-[42%] so it never consumes
the full left column. The container list scrolls internally via Sencho's
existing Radix ScrollArea.
- A summary strip shows total / running / paused / unhealthy counts above
the list, with a Compact / Detailed density toggle. Detailed (default)
shows CPU / Mem / Net sparklines; Compact hides them for a denser list.
- The logs pane has a guaranteed min-h-[180px] so it is always reachable.
- Single-container stacks are untouched: the card keeps its original
shrink-0 behaviour, and no summary strip or density toggle appears.
Mobile is unaffected (it already uses segmented Health / Logs / Compose
panes).
* fix: key ContainersHealth by node+stack so density resets on navigation
The density toggle state (compact/detailed) is local to ContainersHealth.
Without a key, selecting Compact on one multi-container stack would persist
when navigating to a single-container stack, hiding sparklines while also
hiding the toggle itself. Keying by node+stack ensures a clean remount.
Also added test coverage for zero containers and density reset on remount.
* fix: remove unused rerender variable from ContainersHealth test
* 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.
* fix: fall back to enroll token when pilot tunnel JWT is rejected
On HTTP 401/404 upgrade rejection, delete stale pilot.jwt and retry with SENCHO_ENROLL_TOKEN.
* test: import pilot agent module after DATA_DIR is set in fallback test
The auth-fallback test statically imported pilot/agent, which freezes its
pilot.jwt path from DATA_DIR at module load, before setupTestDb redirects
DATA_DIR to a writable temp dir. On the Linux CI runner the path resolved
to a non-writable /app/data, so persistToken silently failed and the
round-trip assertion read null. Import the module dynamically in beforeAll
after setupTestDb, matching the sibling unit test.
* fix: remove unexpected-response listener that blocked pilot reconnect
The ws library skips abortHandshake when an unexpected-response listener
exists, so error and close never fire and the agent hangs in CONNECTING.
Detect auth rejection via the abortHandshake error message instead; the
close handler already performs enroll-token fallback and reconnect.
* 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