Commit Graph

637 Commits

Author SHA1 Message Date
Anso 454e647a3c docs: write Give a Teammate Deploy Access to One Stack tutorial (#1800)
Adds a new Tutorials entry (not part of the original 14-page batch)
covering scoped RBAC permissions: create a Viewer account, grant a
Deployer scope on one stack, and verify the boundary from both the
teammate's session and the audit log.
2026-08-08 13:12:45 -04:00
Anso 512370e3f4 docs: v1 docs refresh (batch 8) (#1797)
* docs(rbac): refresh Users panel screenshots to current build

Recapture users-list, add-user-form, role-selector, and delete-confirm
against the live app so they reflect the current Community RBAC parity
UI. Update alt text to match.

* docs(sso): refresh SSO settings screenshots and document last-provider lockout guard

Screenshots were captured against a stale build; refresh with current UI.
Also documents the guard that blocks disabling or removing the last
enabled SSO provider while SSO-only mode is active, and corrects the
Recovery settings path (Operations, not System).

* docs(sso): refresh SSO quickstart screenshots to current build

Screenshots were captured against a stale build; recapture the provider
overview and the LDAP test-connection failure state against current UI.
Page text already matched the implementation, so no content changes.

* docs(alerts): refresh Alerts and Notifications for current RBAC and retention behavior

Verified every claim against current backend/frontend code and the live app.
Corrected two factual errors: stack alert rule add/delete visibility now
follows the stack:edit permission (roles renamed since the last refresh,
not an admin-only gate), and the notification_history cap is a 500-row
per-stack / 1000-row per-node-unattached pair enforced on the monitor's
30-second cleanup pass, not a flat 100-row per-node cap evicted on insert.

Added the stack_taken_down category (also part of the built-in bell
quieting rule, previously undocumented) and the seven history-only
categories that are filterable but never dispatched externally. Noted the
two vulnerability-scan retention controls now sharing the Data Retention
page, with a cross-link to their own doc.

Recaptured all eight screenshots against the production node.

* docs(fleet-sync): remove stale Admiral gate claim, refresh replica screenshots

Fleet Sync's replication, anchor, and demote mechanics have been free on
Community since the paid gates were dropped from sync-status and
reset-anchor; the Prerequisites table still claimed the control instance
needed Admiral. Also documents the one-click "Reset anchor on peer"
recovery banner on Settings -> Nodes, verified live against a real
control/replica pair, and replaces all four screenshots with current
captures.
2026-08-08 13:12:11 -04:00
Anso b23c413768 docs: correct webhook and registry tier claims in OpenAPI spec (#1799) 2026-08-08 09:53:54 -04:00
Anso cc2d215b2d docs(self-hosting): document Docker socket proxy permission profiles (#1798)
Adds a "Docker socket proxy" section to self-hosting that maps Sencho
features to the Docker Engine API groups a proxy must allow. Three
profiles (monitoring / minimum management / full) with literal flag
values, a feature-to-API reference table, a keep-disabled list, and the
self-update caveat: helpers always mount the host Unix socket, so a
TCP-only proxy does not carry update traffic.

Also adds a production-hardening checklist item pointing at the section,
cross-links from the feature pages whose behavior depends on mutating
Docker access (mesh, file explorer, resources, remote updates, stack
management, scheduled operations), and a pointer from Compose Doctor's
proxy findings to the sizing guidance.

Closes #1796
2026-08-08 03:17:56 -04:00
Anso de7ecee497 docs: tutorials batch 1 (#1656)
* docs: scaffold Tutorials tab and write enroll-a-remote-node

Adds the Tutorials tab to docs.json with 15 stub pages across three
groups (Fleet & nodes, Deploy & automate, Secure & integrate), and
writes the first full tutorial: enrolling a remote node via Pilot
Agent mode, verified end to end against a live control instance and
a second host running an existing Jellyfin Compose stack.

* docs: write Schedule an Operation tutorial

* docs: fix MDX parse error in Schedule an Operation tutorial

* docs: write Set Up SSO with Custom OIDC tutorial

Registers an OAuth client in a self-hosted identity provider (Keycloak
worked example), configures Sencho's Custom OIDC settings, tests the
connection, and verifies a real end-to-end login with auto-provisioning
from two independent surfaces.

* docs: drop unused SSO tutorial screenshot

sso-settings-empty.png isn't referenced by the tutorial content.

* docs: write Set Up Fleet Federation tutorial

Migrates a Blueprint-managed workload from one node to another using
pin and cordon, with the confirm-before-mutate rollout in between.
Corrects the published feature page's claim that pin requires the
global admin role; the code gates cordon and pin identically, scoped
to the target node.

* docs: write Create and Approve a Blueprint tutorial

Covers labeling a target node, authoring a stateless Blueprint,
walking through the create-then-approve rollout flow, verifying
from the Deployments tab and the audit log, and recovering from a
port-conflict deploy failure. Cross-links with Move a Blueprint
Deployment to a New Node in both directions.

* docs: write Automatically Patch a Stack With an Auto-Update Label tutorial

* docs: write Configure Auto-Heal Policies tutorial

Adds the full step-by-step content for the Configure Auto-Heal Policies
stub: an nginx+redis scenario stack, adding a service-scoped policy,
and a live verification that breaks a container's healthcheck,
confirms the policy restarts it, and recovers it.

* docs: write Set Up Deploy Enforcement tutorial

Covers configuring a block-on-deploy scan policy against a stack running
a deliberately outdated nginx image, reading the block dialog, and
overriding it as an admin with the bypass confirmed in the audit log.
Includes a stack-pattern mismatch as the most likely first-time failure.

* docs: write Configure Environment Guardrails tutorial

Covers the Block deploy on missing required env vars guardrail end to
end: deploy a Postgres stack with a required password, enable the
guardrail, watch a real update get refused with a named-variable
message, fix it, and verify from the Activity and Environment tabs.

* docs: write Deploy a Stack Automatically From Your CI Pipeline tutorial

* docs: write Catch and Fix a Container That's Drifted From Its Compose File tutorial

Covers reading a real Drift finding after an out-of-band container
change and resolving it by redeploying through Sencho.

* docs: write Connect a Git Source tutorial

* docs: write Push a Shared Environment File to Every Node tutorial

Writes the Fleet Secrets tutorial: create a bundle, target nodes by
label, read the push preview/results, verify via the audit log, and
recover from a stack-name typo. Removes the three unwritten
placeholder stubs (RBAC, Sencho Mesh, private registries) that had no
scheduled content.
2026-08-08 01:49:21 -04:00
Anso bb98cba1f2 fix(compose-doctor): recognize Docker socket proxy topologies (#1791)
* fix(compose-doctor): recognize Docker socket proxy topologies

Classify dedicated socket proxies separately from direct docker.sock mounts so Doctor no longer recommends adopting a proxy the stack already uses. Closes #1790.

* fix(compose-doctor): widen socket proxy detection and flag writable proxy sockets

Close the remaining gaps in socket proxy topology handling: a service that
points at a proxy through a tcp:// endpoint on its command line (how Traefik
and friends do it) now gets the client note, proxy API group flags are read
for any truthy value rather than a literal 1, and underscore or dot separated
proxy names are recognized.

Two cases that previously slipped through now surface: a service classified
as a proxy purely by name or image but mounting docker.sock read-write is
reported as high, and a proxy on the implicit default network or on a network
the rendered model does not describe counts as non-internal. A direct socket
mount alongside an existing proxy now names that proxy in its fix.

* fix(compose-doctor): require corroboration before a service name classifies a socket proxy

A service name is free text the author controls, so on its own it could move a
writable docker.sock mount out of the high direct-mount finding. A known proxy
image is an artifact identity and still stands alone; a proxy-shaped name now
counts only alongside an observable fact, a read-only socket or a scoped API
group key.

* fix(compose-doctor): tighten socket-proxy detection against live upstream behavior

Require proxy API flags to be exactly 1 (matching tecnativa and linuxserver
images), count only those enabled flags when classifying a proxy, extract
tcp hosts from DOCKER_HOST instead of treating key presence as a proxy client,
and correlate each client note to one proxy instance by both name and shared
network. Soften the published-port finding so it claims reachability rather
than Docker API exposure for unrelated ports.
2026-08-08 01:32:26 -04:00
Anso 5c52ae26eb fix(rbac): make complete built-in RBAC available on Community (#1793)
Open all five built-in global roles and stack/node scoped assignments
on Community. Remove paid fences from user role create/update, scoped
assignment CRUD, permission evaluation, and the Users settings UI.

Admiral continues to own extended audit governance, LDAP directory
integration, and other organizational assurance features. Built-in
scoped RBAC is no longer marketed or enforced as paid-only.
2026-08-07 23:50:53 -04:00
Anso 4fa532530e fix(image-updates): explain persistent digest rebuilds after update (#1784)
* fix(image-updates): explain persistent digest rebuilds after update

When an update completes but a same-tag digest rebuild is still detected,
the generic "update still detected" warning told operators nothing about
why. The digest comparison already knows the remaining updates are
digest-only (no higher tag), so recheckStack now returns a targeted
warning naming the two daemon-side causes: a registry mirror or cache
serving stale content, or a container still pinned to the previous image.

The digest-rebuild badge surfaces (Anatomy banner, Fleet cards, mobile)
now carry a tooltip with the same explanation, and the post-update
warning is added to the pre-update refresh sanitization set.

* fix(image-updates): surface digest warnings on editor and mobile paths

Editor Update discarded recheckWarning, digest hints were hover-only, and
service-scoped rechecks blamed the daemon when only sibling services remained stale.
2026-08-06 09:22:53 -04:00
Anso 4e54a9272d feat: add inspect image action on stack container cards (#1780)
Open the existing image detail sheet from each container card, between View logs and Monitor, so operators can inspect a container's image without leaving stack details.
2026-08-05 18:10:11 -04:00
Anso 85b841175a feat: make the Files explorer tree pane resizable (#1774)
* feat: make the Files explorer tree pane resizable

Operators can drag the Files tab divider to read long or deeply nested names. The width is remembered in this browser; names that still overflow scroll horizontally.

* fix: keep Files tree resize from leaking cursor or stealing row clicks

Unmount and lost-pointer-capture now clear the drag gesture. The stored width stays a preference while layout clamps to the explorer size, and the hit target only expands into the viewer so full-row context menus still work.

* fix: persist last live Files pane width on pointercancel

Cancel events can report clientX 0, which was clamping the tree to 160px and writing that to storage. Commit the last tracked width instead, same as lostpointercapture.
2026-08-05 12:50:11 -04:00
Anso a826cd398d fix: allow chmod on protected stack files (#1772)
* fix: allow chmod on protected stack files

Identity protection still blocks delete, rename, and copy-onto-reserved-name for compose and .env at the stack root. Permission changes are ordinary edits and must succeed from the explorer.

* fix: gate chmod on compose files during stack ops

Chmod on compose filenames and .blueprint.json now follows the same
stack-op lock as content writes and uploads. Document allowed blueprint
chmod and that content saves reset mode bits.
2026-08-05 09:22:36 -04:00
Anso a29d451875 fix(resources): attribute free images to managed prune by repository family (#1769)
* fix(resources): attribute free images to managed prune by repository family

After a stack update, unused previous tags lost compose labels and dropped out of managed prune. Match free image repositories still used by managed containers (excluding foreign Compose projects), and clarify the fleet dry-run unlock on the prune card footer.

* fix(resources): omit stackName on repo-match managed prune items

Repository sharing is not ownership; the confirm list must not show stack attribution for repo-matched free images. Pin repository-key normalization with exact Set assertions so the duplicated parser cannot drift silently.

* fix(fleet): bind prune fingerprints to image tag sets

RepoTag churn on an already-planned image Id no longer leaves the dry-run fingerprint unchanged, so fleet preflight rejects the whole execute when any node retags. Itemized delete also refuses on reference drift and reports multi-repository refuse without implying a clean no-op.
2026-08-04 16:38:37 -04:00
Anso 92d974b13e fix: condition --volumes in downStack() on the removeVolumes option (#1764)
* fix: condition --volumes in downStack() on the removeVolumes option

ComposeService.downStack() hardcoded --volumes on every stack delete,
ignoring the "Also remove associated volumes" checkbox and destroying
volumes the operator asked to keep. The sibling Take-down path (runDown)
already conditions --volumes correctly.

- Add options?: { removeVolumes?: boolean } to downStack()
- Default to data-preserving (no --volumes when option absent)
- DeletedStackDeletionService reads the persisted intent flag
- Templates rollback passes removeVolumes: true (clean up failed deploy)
- Blueprint withdraw passes removeVolumes: false (volumes preserved)

* docs: update Delete row to reflect conditional volume removal

The Delete row now describes that volumes are removed only when the
operator opts in, matching the behavior introduced by the downStack fix.

* fix: add capability gate for delete pruneVolumes and fix QA findings

Four P0 issues found in live QA:

P0-1/P0-4 - No capability gate on delete's pruneVolumes:
  Add stack-delete-prune-volumes capability so the frontend hides the
  "Also remove associated volumes" checkbox on nodes that don't support
  conditional volume removal on delete. Without this, an operator on an
  old node sees a VOLUMES KEPT promise the old node silently breaks.
  Frontend-only gate: no API or proxy gate because the old node's
  fallback (always destroy) is correct for the checked case.

P0-2 - Checkbox state leaked across dialogs:
  Reset pruneVolumes in onConfirm before calling the parent, so a
  previously checked box doesn't appear pre-checked when the dialog
  opens for a different stack.

P0-3 - Delete not bound to the active node:
  Capture activeNode.id at delete time and pass it as an explicit
  nodeId to apiFetch, matching the Take Down pattern. Without this,
  switching the active node while the dialog is open silently deletes
  the wrong stack on the wrong node.

* fix: update test assertions for nodeId binding and showVolumeOption gate

P0-3 added nodeId to apiFetch DELETE calls — two useStackActions tests
now expect the parameter. P0-1 gated the volume checkbox behind
showVolumeOption — the confirming test now passes the prop.

* fix: gate volume hint on showVolumeOption to prevent false promise

On nodes without stack-delete-prune-volumes, volumes are always
destroyed. Showing VOLUMES KEPT was a lie. Now the hint is hidden
entirely when the capability is absent.

* fix: gate delete against nodes that cannot guarantee volume preservation

Hiding the checkbox and the misleading hint stopped the false promise but
not the data loss: an unchecked delete against a node lacking
stack-delete-prune-volumes still reached that node and its downStack()
still destroyed volumes unconditionally, now with no warning at all.

- remoteNodeProxy.ts: block an unacknowledged DELETE /stacks/:name
  (no pruneVolumes=true) to a remote lacking the capability, mirroring
  the existing removeVolumes gate on the down route. An explicit
  pruneVolumes=true always proxies through since that matches what an
  unsupported remote does anyway.
- DeleteStackDialog: rework around a three-state model (supported /
  unsupported / unknown) instead of a boolean. A node whose capabilities
  have not been confirmed (meta not yet fetched, or a failed probe) is
  now treated like a supported node, not forced onto the destructive
  path just because its state is unresolved.
- Fix deleteStack's error toast, which surfaced the raw JSON response
  body instead of the parsed error message.
- Fix CreateStackDialog's orphan-stack rollback (docker-run import),
  which silently no-op'd against a node requiring acknowledgement.
- Update node-compatibility.mdx and stack-management.mdx to describe
  the new gate.

* test: advertise stack-delete-prune-volumes on the scoped-evidence fixtures

These mock remotes simulate nodes capable enough to run scoped-stack-auth-evidence
RBAC and were pinned before stack-delete-prune-volumes existed, so the new delete
gate now blocked their unacknowledged DELETE calls before reaching the mock server,
failing the grant-tuple-cleanup assertions the tests actually check.
2026-08-04 13:04:21 -04:00
Anso 0ba09ebdee feat: add ntfy notification channel (#1761)
* chore: bump brace-expansion and fast-uri via npm audit fix

Resolves GHSA-rgw5-rvv9-x895 (brace-expansion DoS via unbounded
intermediate arrays). Both transitive dev dependencies updated:
- brace-expansion 5.0.8 -> 5.0.9
- fast-uri 3.1.4 -> 3.1.5

* chore: also bump frontend deps via npm audit fix

Fixes brace-expansion and postcss in the frontend lockfile so
npm audit --audit-level=high passes on both packages.

* chore: bump ip-address transitive dep via npm audit fix

Resolves three new ip-address advisories (GHSA-mwp4-54f8-5fhr,
GHSA-4xrf-jv44-h6hh, GHSA-22jq-vg5j-6vgg) published between prior
push and CI run.

* feat: add ntfy notification channel

Add ntfy (https://ntfy.sh) as the fifth notification channel alongside
Discord, Slack, Webhook, and Apprise. ntfy speaks its native protocol:
plain-text POST body with Content-Type, Title, Priority, and Tags
headers. Priority maps info/warning/error to ntfy's default/high/urgent.

URL validation allows both HTTP and HTTPS (common for LAN self-hosting)
but rejects embedded credentials, consistent with Apprise. Token auth
via ntfy's documented ?auth= query parameter is supported.

* fix: correct ntfy channel test cases for Linux URL parsing and required type field

- notification-channels.test.ts: replace http:///topic host check with a
  cross-platform invalid-URL case (WHATWG parser treats triple-slash
  authority differently on Linux vs Windows)
- ConfigurationStatus.test.tsx: add ntfy agent slot to makePayload and
  inline agents fixtures (required by the expanded ConfigurationAgents
  type)

* fix: remove unused import and update 0/4 masthead assertions to 0/5

* ci: exclude NotificationService.ts from js/request-forgery CodeQL rule

Notification channel dispatch methods (Discord, Slack, Webhook, Apprise,
ntfy) all call fetch() with admin-configured URLs and notification bodies
that may embed stack or path data. This matches the trust model already
documented for registry-api.ts: single-tenant self-hosted, admin owns
the server, outbound posting is the intended behavior. The write path is
gated by requireAdmin or requirePermission(node:manage), and every
dispatch runs with a 10s AbortSignal.timeout.

* ci: also exclude NotificationService.ts from js/file-access-to-http

Notification messages may embed stack names, paths, or compose-derived
content. Same trust model as js/request-forgery: admin owns the server
and the configured endpoints, write path is gated.

* fix: correct ntfy channel tab copy and validation error message

The ntfy settings tab was reusing the generic webhook label, helper, and
placeholder (Webhook URL / JSON payloads / https://...). Give ntfy its own
copy: label names the server-and-topic URL, helper states plain-text delivery
and the mandatory topic path, placeholder matches the routing section.

Also fix the routing-rule validation toast: the guard correctly exempts ntfy
from the HTTPS check but the error message was not updated alongside it, so
ntfy URLs received a misleading HTTPS-required message.

* fix: strip trailing slash from ntfy topic URL before dispatch

A topic URL like https://ntfy.sh/mytopic/ validates fine (the check strips
the trailing slash internally) but was stored and dispatched with the slash
intact, causing the real ntfy server to 404. Normalize before fetch so the
request reaches the correct topic path.

Also add ntfy to the Channels card description in the settings registry.
2026-08-03 19:29:41 -04:00
Anso 71be1634a5 feat: surface ZFS ARC reclaimable as dashboard context line (#1760)
* feat: surface ZFS ARC reclaimable as dashboard context line

Add arcReclaimable to the HostMemory interface and MemoryWire shape so
the reclaimable ARC amount computed by readReclaimableArc() is exposed
through /api/system/stats and /api/fleet/overview. Show it as a context
line on the dashboard memory tile, matching the balloon pattern.

ARC continues to feed the gauge percentage as before; this is a
display-only addition for operator visibility.

* docs: clarify ARC line requires nonzero reclaimable, not just readable stats
2026-08-03 13:44:20 -04:00
Anso 1c9060a855 fix(ui): flatten single-container Update onto the service row (#1759)q
* fix(ui): flatten single-container Update onto the service row

On multi-service stacks, put Update/Rebuild on the container card (left of
image source) when a service has one matching container, and keep the shared
header only for multi-replica services.

* fix(ui): show per-service Update only when an image update is confirmed

Registry services were always showing Update because eligibility checked
declaredImage/hasBuild only. Gate Update on a confirmed pending check so
the button clears after a successful recheck; keep Rebuild for build-backed
services.
2026-08-03 10:10:09 -04:00
Anso 5f5cd5ea51 feat(fleet-secrets): graduate encrypted fleet-wide environment bundles to Community (#1758)
* feat(fleet-secrets): graduate encrypted fleet-wide environment bundles to Community

* fix(fleet-secrets): update reachability test for Secrets community graduation

* fix(fleet-secrets): address review findings

* fix(fleet-secrets): add HTTP-level Community admin push/import tests and non-admin tab-hidden test
2026-08-03 08:42:46 -04:00
Anso 41bf075eb0 feat(recovery): make rollback-recovery image lifecycle visible and controllable (#1753)
* feat(recovery): make rollback-recovery image lifecycle visible and controllable

GitHub discussion #1751 asked why Sencho creates sencho-rb/<id>/<service>:hold
images during automatic updates and how to clean them up. That surfaced a real
safety bug alongside the missing visibility: the manual single-image delete
route did not consult the held-image predicate every other deletion path
already honors, so a user could delete a rollback-protected image straight
through the Images tab and silently break automatic recovery for that update.
A short/truncated id also bypassed the predicate's full-id lookup.

Fixes:
- POST /images/delete now resolves the submitted id to its canonical form and
  checks the unified held-image predicate before deleting, returning 409
  IMAGE_HELD_FOR_ROLLBACK for a protected image.
- The Images tab no longer mislabels a protected image as plain "Unused"; a
  fully-synthetic hold image is kept out of the generic inventory entirely and
  surfaced instead in a new Resources -> Rollback tab, with an additive
  "Rollback protected" badge for images that still carry a normal tag too.

New capability:
- Two settings (Deploy Guardrails): superseded-generation retention (days,
  replaces a hardcoded 7) and a cap on retained generations per stack.
- A new Resources -> Rollback tab lists every generation (stack, short id,
  state, retention) with an admin-gated manual release action, including
  releasing the current generation with an explicit warning that automatic
  rollback becomes unavailable until the next successful update. Release is
  a single atomic, server-revalidated transition so a stale UI read can never
  release a row that has since become ineligible.

Also consolidated three near-duplicate implementations of the held-image
predicate (two of which relied on a require() of a sibling .ts file that
silently failed to resolve under the test runner and was never actually
exercised by a real test before this change) into one shared module.

Known follow-up, not fixed here: an orphaned sencho-rb tag whose recovery row
no longer exists (DB restore, node re-add) is invisible in both the Images
and Rollback tabs with no UI path to reclaim it.

* fix(audit): add summary mapping for rollback generation release

* fix(security): sanitize prune target in log sinks and cover release RBAC

Closes two open js/log-injection findings on the system prune route by
applying the same inline sanitizeForLog barrier the rest of the file
already uses. The prune target is validated against an enum by
parsePruneTargets before reaching these sinks, so the findings were false
positives, but the barrier is cheap and removes the standing alerts on a
file this change already touches. Also wraps the generation id in the
release log line for consistency with the stack name beside it.

Adds coverage for gaps a QA pass identified:
- Release endpoint refuses a viewer and a deployer (Admin-only), leaving
  the generation and its artifacts untouched.
- Viewer can still read the generations list, matching the sibling
  Resources routes.
- The predicate the prune routes build reports full-stack rollback holds,
  not just service-scoped ones, and re-reads per call so a hold taken
  between plan and delete still gates the delete.
- After releasing the current generation, no rollback point is claimed
  for the stack through any consumer of the current-generation lookup.
2026-08-02 21:55:22 -04:00
Anso 97be019696 feat(fleet): add Node details sheet to the node card kebab (#1752)
* feat(fleet): add Node details sheet to the node card kebab

Every Fleet node card now carries a "Node details" kebab item, open to
any role that can see the card (previously the kebab only rendered
for users with node-manage permissions, so plain viewers had none).
The sheet shows connectivity, live capacity, Compose workload,
version/capability compatibility, and governance info (labels, cordon
reason and date, default-node flag, Compose directory, registration
date) using data the Fleet page already fetches, plus one lazy call
to the existing node meta endpoint for capabilities. Wired into both
the desktop card and the mobile bespoke Fleet screen.

* fix(fleet): correct Node details sheet timestamp units and update-status fallback

QA against a live 3-node fleet found that last_successful_contact and
pilot_last_seen come back from the fleet-overview endpoint in Unix
seconds, but the sheet passed them straight into a milliseconds-only
formatter, rendering values like "20647d ago" instead of "just now".
Both are now converted before formatting.

The Compatibility section's update-status badge also fell through to
a confident "Up to date" whenever updateStatus was absent (e.g. on
mobile, which doesn't poll update status) instead of reflecting that
there was no data to back the claim; it now renders "Unknown" in that
case. The local node no longer shows a misleading "Last successful
contact: Never". Reworded the "read-only sheet" language in the docs
page to describe the sheet accurately, since the Governance section's
label picker stays editable for node managers by design.
2026-08-02 21:35:50 -04:00
Anso c613010199 feat: account for VM memory ballooning in host memory reporting (#1750)
* feat: account for VM memory ballooning in host memory reporting

Extend hostMemory.ts with a readBalloonedMemory() function that parses the
Balloon: field from /proc/meminfo, following the same fail-open pattern as
the ZFS ARC integration. When a nonzero balloon is detected, effective
memory fields (effectiveUsed, effectiveFree, effectiveUsagePercent) are
computed and exposed through /api/system/stats and /api/fleet/overview.

All consumers that derive meaning from host memory now prefer effective
values when present: the dashboard gauge, Fleet card RAM bar, mobile
views, health verdict, health status bar stat tile, and host RAM alerts.

Backward compatible: missing /proc/meminfo or absent Balloon: line
preserves exact current behavior. Old remote nodes without the new fields
continue rendering normally.

* refactor: extract shared helpers for balloon memory wiring

Extract readCandidateFile() and logSelectedPath() in hostMemory.ts to
deduplicate ARC and balloon file-read logic. Add memoryToWire() to
centralize the optional-field spread used by /api/system/stats and
/api/fleet/overview. Add getNodeMemUsed()/getNodeMemTotal() helpers
in nodeUtils.ts for frontend byte-text consumers.

* fix: make desktop fleet masthead aggregate balloon-aware

The desktop fleet overview's memory aggregate in useFleetOverview.ts still
summed raw memory.used, while the mobile fleet aggregate and per-node cards
already used effective values. Update to use getNodeMemUsed/getNodeMemTotal
helpers.

* fix: revert balloon adjustment from alerting and health decisions

Ballooned memory is host-reclaimed (unlike ZFS ARC, which the guest can
reclaim on demand). The guest cannot get ballooned pages back until the
hypervisor deflates them, so treating ballooned memory as available for
alerting or health can mask real memory pressure.

Keep balloon parsing, wire fields, and the dashboard context line as
informational-only. The memory gauge, health verdict, and host RAM alerts
now use the standard ARC-adjusted working-set percentage regardless of
balloon. Updated configuration.mdx and dashboard.mdx to document that
balloon data is informational and does not influence alerting.
2026-08-02 20:48:38 -04:00
Anso 15801318d6 fix(rbac): permission-gate alerts, auto-heal, and image updates (#1743)
* fix: gate alerts and auto-heal routes on stack:edit/stack:read permissions

Replace requireAdmin with requirePermission across backend/src/routes/alerts.ts
and backend/src/routes/autoHeal.ts, mirroring the stack:read/stack:edit model
already used by stacks, blueprints, git sources, and settings. Adds the
previously-missing permission gate on the auto-heal history route, and adds
ownership-aware deletion for alerts via a new DatabaseService.getStackAlert(id)
lookup.

* fix: gate image-update fleet, per-stack refresh, and auto-update execute on RBAC permissions

Replace requireAdmin with requirePermission/checkPermission across
backend/src/routes/imageUpdates.ts (imageUpdatesRouter and autoUpdateRouter),
mirroring the permission-aware model already used by alerts and auto-heal.

GET /fleet drops its admin gate to match the auth-only read model shared with
GET / and /detail. POST /fleet/refresh now requires node:manage. A new route,
POST /refresh/:stackName, lets a caller with stack:deploy on that stack trigger
a per-stack recheck, distinct from the node-wide POST /refresh. The auto-update
executor now pre-checks stack:deploy across every resolved target before any
work starts, so a denied stack in a bulk request fails the whole call instead
of partially executing; the "*" wildcard additionally requires global
stack:deploy up front since it expands to every stack on the node, including
the empty case where a per-stack check would otherwise have nothing to gate.

* fix: evaluate permission before checks-enabled state in auto-update execute

The checks-enabled short-circuit in autoUpdateRouter POST /execute ran before
target parsing and before any permission check, so a node with image-update
checks disabled returned 200 to any authenticated caller regardless of
stack:deploy grants. Move the checks-enabled check to run after the resolved
stackNames have cleared requireExactStacks, so permission is always evaluated
first.

Add coverage: a denied role still gets 403 PERMISSION_DENIED (not the
disabled-checks 200) while checks are disabled node-wide, and a scoped-only
user whose stack:deploy grant covers every stack on the node is still denied
target="*" (the wildcard requires global stack:deploy, per the earlier fix),
proving that tradeoff against a real on-disk stack rather than the always-
empty fresh test instance.

* fix: gate alerts, auto-heal, and image-update controls on frontend permission checks

Match the backend RBAC gates for alerts, auto-heal, and per-stack image
updates with matching frontend checks, replacing raw isAdmin/node:manage
gates with scoped can() calls:

- Alerts/Auto-Heal menu items and their keyboard shortcuts now gate on
  stack:read (canViewMonitor), including the window-level keyboard
  shortcut handler that previously bypassed the menu item gate entirely.
- Check updates now gates on stack:deploy (previously node:manage) and
  calls the new per-stack POST /image-updates/refresh/:stackName
  endpoint instead of the node-wide refresh. Since the endpoint runs the
  recheck synchronously and returns the result directly, the old
  node-wide /status polling loop is removed in favor of handling the
  response inline.
- StackAlertSheet's alert and auto-heal policy mutation controls gate on
  stack:edit instead of isAdmin.
- The Fleet Image Updates refresh button (mobile and desktop) gates on
  node:manage, hidden rather than disabled to match the existing
  convention for node:manage-gated affordances.

* fix: cover the stack:edit deny path for StackAlertSheet gates

The useAuth mock in StackAlertSheet.test.tsx returned can: () => true
unconditionally, so canEditAlerts, canEditAutoHeal, and PolicyRow's
canEdit prop were never exercised with a denial. Make the mock
per-test-controllable (matching the vi.fn() pattern already used in
NodeCard.test.tsx) and add one deny-path test per tab asserting the
mutation controls are absent while reads stay visible.

Also adds an aria-label to the alert row's delete button so the deny
test can assert on its absence, matching the aria-label convention
PolicyRow's own toggle/delete controls already use.

* fix: surface accurate warnings and loading feedback on stack update checks

checkUpdatesForStack ignored the backend's StackRecheckResult outcome
and always showed a success toast, even when verification failed or
an update is still present. It also gave no feedback while the
multi-second per-image registry probe was in flight.

Add a loading toast on request start, and branch the result toast on
outcome/warning instead of unconditional success. The backend reuses
its post-update reconciliation copy for this pre-update discovery
check, so the two generic "update command completed" strings are
replaced with accurate pre-update wording; a genuine stack-specific
warning (e.g. a compose render failure) is still shown as-is.

Also update docs/features/rbac.mdx: stack:edit now covers alert and
auto-heal management, stack:deploy covers per-stack image-update
checks, and the Deployer role description reflects both.

* fix: add per-stack cooldown rate limit for image-update recheck route

The per-stack POST /refresh/:stackName route bypassed the existing
node-wide manual-refresh cooldown. A caller with stack:deploy could
hammer the registry with unbounded concurrent recheck calls.

Add tryMarkStackRecheck in ImageUpdateService, sharing the same
2-minute cooldown window, keyed per (nodeId, stackName). The route
handler returns 429 when denied. The mark is written synchronously
before the first await so concurrent calls on the same tick are blocked.
2026-08-01 21:07:21 -04:00
Anso a1e2846d7d feat: expose Community audit log via system:audit navigation (#1740)
* feat(rbac): make Settings authorization permission-aware

Align Settings visibility and mutations with the existing permission matrix so Node Admin can edit node-scoped operational settings while system and credential surfaces stay Admin-protected.

* fix(rbac): tighten settings permission buckets and tests

Collapse settings key permission maps into one source of truth, and cover mixed PATCH atomicity plus image-update enabled writes.

* fix(rbac): tighten Settings scoped grants and CI assertions

Empty settings PATCH fails closed, node:manage is scoped to the active
node, system-only Settings stay hidden without system:settings, and
Check updates / webhooks mutate gates follow the permission matrix.

* fix(rbac): defer Settings section fallback until authz is ready

Keep deep links to permission-gated sections (e.g. license) intact while
can() is still fail-closed during permission metadata load.

* feat: expose Community audit log via system:audit navigation

Gate the Audit view on the system:audit permission instead of paid tier,
so Community admins can open the existing 14-day recent-activity window.
Export, anomaly flags, and stats remain Admiral-only.

* test: clarify synthetic Community admin mock lacks system:audit

Document that mockCommunityAdmin is a gate-isolation helper, not the
real Admin permission matrix where system:audit is always present.
2026-07-30 12:50:55 -04:00
Anso a3026f47a8 feat(rbac): make Settings authorization permission-aware (#1738)
* feat(rbac): make Settings authorization permission-aware

Align Settings visibility and mutations with the existing permission matrix so Node Admin can edit node-scoped operational settings while system and credential surfaces stay Admin-protected.

* fix(rbac): tighten settings permission buckets and tests

Collapse settings key permission maps into one source of truth, and cover mixed PATCH atomicity plus image-update enabled writes.

* fix(rbac): tighten Settings scoped grants and CI assertions

Empty settings PATCH fails closed, node:manage is scoped to the active
node, system-only Settings stay hidden without system:settings, and
Check updates / webhooks mutate gates follow the permission matrix.

* fix(rbac): defer Settings section fallback until authz is ready

Keep deep links to permission-gated sections (e.g. license) intact while
can() is still fail-closed during permission metadata load.

* docs(settings): clarify Notifications channels vs routing authz

Channels use node:manage via /api/agents; routing and mute stay Admin-only.
2026-07-30 10:25:13 -04:00
Anso a65bf4d46a fix(rbac): handle permission metadata failures (#1735) 2026-07-29 15:51:27 -04:00
Anso 44d6078241 feat(fleet): show itemized prune plans (#1734)
* feat(fleet): itemize prune review plans

Build and display fingerprint-bound prune candidates for every reviewed
fleet node. Preflight all node plans before mutation and preserve detailed
removed, skipped, failed, and partial outcomes.

Add safe resource metadata projection, managed ownership attribution,
runtime contract validation, transport parity coverage, and operator docs.

Closes #1724

* fix(security): harden stack path lookup

Use a Map for Compose working-directory ownership resolution so untrusted
path strings cannot become object property writes.

* fix(fleet): harden prune execution safeguards
2026-07-29 14:30:18 -04:00
Anso 3c934066f3 fix(networking): ignore verified Mesh attachments in drift (#1729) 2026-07-29 12:55:30 -04:00
Anso 9922d8e765 feat(rbac): make stack-scoped grants node-specific (#1727)
* feat(rbac): make stack-scoped grants node-specific

Qualify stack role assignments as (nodeId, stackName), migrate legacy rows to the default node, and forward bound multi-action evidence on Proxy/Pilot hops so scoped users keep least-privilege remote access without shipping the full grant table.

* fix: mirror scoped-stack-auth-evidence capability to frontend, sanitize node id in role assignment log

Backend added the scoped-stack-auth-evidence capability without the
matching frontend entry, failing the capability parity test. The role
assignment log also interpolated the node id without sanitizeForLog,
unlike the rest of the line.

* fix(rbac): honor node-wide scopes and fix proxied DELETE cleanup

Node-scoped grants now authorize that role's stack actions on the same node in the backend resolver, frontend can(), and remote evidence. Proxied DELETE cleanup uses the gate-stashed route because pathRewrite mutates req.path before proxyRes. Add proxy integration coverage and drop the stale scoped-permissions screenshot.

* fix(rbac): preserve node-qualified grants during repair
2026-07-29 09:42:14 -04:00
Anso b0b423b234 feat(fleet): reapply Compose configuration without a version update (#1716)
* feat(fleet): reapply Compose configuration without a version update

Add a distinct Fleet Reapply configuration path so Compose-managed nodes can recreate Sencho from the current on-disk project when already up to date, without pulling or rewriting the image reference.

* fix(fleet): confirm remote reapply and close concurrent tracker race

Require confirmation for remote compose reapply, and lock dispatch before the remote POST so a second request cannot overwrite a successful in-flight tracker.

* fix(ui): icon-only Reapply control so Up to date badge can breathe

Collapse the Node updates Reapply label into a tooltip so the status pill no longer wraps in the Status column.

* feat(editor): Save & Reapply self-stack via fleet compose reapply (#1726)

* feat(editor): Save & Reapply self-stack via fleet compose reapply

Eligible admins can apply on-disk Compose edits to Sencho's own stack from the editor using the same confirm, dispatch, and reconnect path as Fleet Node Updates.

* fix(editor): gate Save & Reapply label to self-stack only

Ordinary stacks were labeled Save & Reapply whenever the node was
reapply-eligible. Require the selected file to be the self-stack for the
toolbar label and diff confirm CTA.

* fix(ui): move compose diff action label helper out of dialog module

Keep ComposeDiffPreviewDialog component-only so react-refresh Fast Refresh
lint passes after the Save and reapply stacked merge.
2026-07-28 14:26:46 -04:00
Anso 543e4ef256 feat(compose-doctor): show All Clear when findings are acknowledged (#1721)
When every finding is acknowledged, the Doctor summary uses the success All Clear banner with distinct copy instead of the muted acknowledged chip, so operators can see at a glance that no active findings remain.
2026-07-28 14:26:16 -04:00
Anso 78475d96ef fix(compose-doctor): resolve effective healthcheck coverage (#1713)
* fix(compose-doctor): resolve effective healthcheck coverage

Compose Doctor now classifies healthcheck coverage from the Compose model, running containers, and local images so image-provided HEALTHCHECKs are not false positives. Update Guard shares the same presence helper so test NONE is not treated as active.

* fix(compose-doctor): fix healthcheck project label and empty compose HC

Use the Compose project name for runtime container listing so stacks whose name: differs from the directory still get runtime evidence. Treat empty or timing-only healthcheck objects as absent rather than active.

* fix(compose-doctor): treat inherited healthcheck as All Clear note

Inherited image healthchecks no longer block All Clear; they surface under a notes section and cannot be acknowledged.
2026-07-28 14:26:00 -04:00
Anso 72cdbb0eaa feat(schedules): auto-update stacks by Stack Label (#1717)
* feat(schedules): auto-update stacks by Stack Label

Add a reusable selector_type/selector_value on scheduled tasks so admins
can schedule image updates against live Stack Label membership across the
fleet or one node, reusing fleet label resolution and the existing
auto-update orchestrator.

* fix(image-updates): sanitize auto-update execute failure logs

Use a static format string and sanitizeForLog so CodeQL no longer
flags user-controlled stack names and error text in the execute catch.

* fix(ui): space Scope label from fleet/node segmented control

Match the Schedule row layout so the inline SegmentedControl no longer
sits flush against the Scope label.

* fix(ui): remove redundant wrapper around Scope segmented control
2026-07-28 13:00:47 -04:00
Anso fa503ddf27 feat: add node-scoped opt-out for image update detection (#1715)
* feat: add node-scoped opt-out for image update detection

Operators who use an external update authority can disable Sencho registry
polling per node without losing explicit stack Update, pull, or redeploy.

* test: fix mocks and lint for image-update checks opt-out

Scheduler tests need isChecksEnabled on the ImageUpdateService mock, and the UpdatesSection older-node fixture must not leave an unused binding.

* fix: gate update-preview and recheck when detection is off

Anatomy was still calling stack update-preview (and contacting registries)
while checks were disabled. Short-circuit those routes and skip recheckStack
writes so disabled nodes stay quiet until detection is re-enabled.
2026-07-28 10:10:04 -04:00
Anso e175db8e62 feat(auth): add SSO-only authentication mode (#1714)
* feat(auth): add SSO-only authentication mode

Let administrators disable interactive local password login when SSO is configured, with backend enforcement, activation safeguards, and host CLI recovery.

Closes #1709

* fix: resolve CI failures in auth mode PR

- Add useLicense mock to SSOSection test to prevent crash from
  AuthenticationModePanel rendering without LicenseProvider
- Remove username from authMode console.log calls that CodeQL flags
  as clear-text logging of sensitive information

* fix(auth): keep SSO-only on named disableSso and fail-closed login

Named provider disable no longer reverts authentication_mode. Login initializes localLoginEnabled false so a status fetch failure cannot reveal the password form. Center a single OIDC provider button on the login card.

* fix(auth): move SSO-only authentication mode from Admiral to Community tier

Security-hardening features belong on the Community tier per the existing
Community rebalance. The reporter of #1709 noted that disabling local
password login after configuring SSO is a basic security measure, not an
enterprise governance feature. LDAP provider configuration remains
Admiral-gated via requireTierForSsoProvider.

* fix(ui): keep SSO Active badge and ON toggle in sync

Provider cards mounted before config fetch finished with enabled:false, so a saved Active provider showed OFF until the local draft was resynced. Drive both the badge and TogglePill from the synced local config.

* feat(auth): auto-redirect to sole OIDC provider under SSO-only

When authentication mode is SSO only and exactly one OIDC provider is enabled (no LDAP), skip the login chooser and send the browser to that provider's authorize URL. Returning sso_error stays on the login page so the failure message remains visible.

* fix(ui): move oidcAutoRedirectUrl out of Login for fast refresh

Exporting the helper alongside the Login component tripped react-refresh/only-export-components and failed Frontend lint CI. Keep Login as a component-only module and colocate the helper with its unit tests under lib/.
2026-07-28 10:01:05 -04:00
Anso 60092dd462 feat: live-refresh stack detail container and health state (#1712)
* feat: live-refresh stack detail container and health state

Keep the open stack's container cards in sync with Docker via state-invalidate events and a visibility-aware poll, without reloading compose, env, or logs.

* fix: remove unused _ms parameter from visibilityInterval mock

Fixes the @typescript-eslint/no-unused-vars ESLint error in CI lint job.

* fix: stop stack detail live-refresh when leaving the editor

Gate poll and invalidate handling on editor visibility, refresh the
current selection after a mid-flight stack switch, and skip starting
visibilityInterval when the tab is already hidden.

* fix: avoid return in finally for stack detail live-refresh

Satisfy no-unsafe-finally by gating the trailing refresh with a positive
condition instead of early returns inside the finally block.
2026-07-28 09:24:18 -04:00
Anso 681ecc7047 fix(auth): keep active sessions alive and add stay-signed-in (#1711) 2026-07-28 07:36:50 -04:00
Anso cfb42af4e0 fix: assorted UI/UX polish fixes (#1670)
* fix(dashboard): replace Stack Health update badge with an icon

The pill badge duplicated space already used by the stack name column.
A CircleArrowUp icon after the name signals an update is available
without competing with the existing ArrowUp/ArrowDown sort indicators
in the same table.

* fix(dashboard): add accessible name to update-available icon

Icon-only indicators need an aria-label directly on the icon; title on
a non-interactive span is not reliably announced by screen readers.

* test(dashboard): cover the update-available icon's accessible name

The icon-only indicator and its aria-label fix had no regression
guard, unlike the equivalent update dot in StackRow.

* refactor(dashboard): compute the update-available label once per row

It was being derived twice (title and aria-label) from the same
row.outdatedServices input.

* fix: drop Community-tier pricing upsells from settings

Community operators no longer see the "See pricing" link in Licensing
or the "Need direct support?" callout in Support. The pricing link now
only shows for an expired paid license needing to renew.

* fix: make Resources images/volumes tables actually scrollable

The tables were wrapped in a Radix ScrollArea sized with max-h-[62vh].
Radix's viewport uses height:100%, which cannot resolve against an
ancestor whose computed height is auto (max-height alone isn't a
definite height), so the viewport silently grew past the visible box
and the extra rows were clipped with no way to reach them. Verified
live: several image rows were permanently unreachable, with no working
internal scrollbar and not enough outer page scroll to compensate.

Switched to an explicit h-[62vh], which the viewport can resolve
correctly, matching every other working ScrollArea in the codebase.
Falls back to h-auto below the md breakpoint so the bespoke mobile
layout keeps shrinking to content and scrolling via the outer page
instead of gaining a fixed-height inner scroll box.

* fix: apply ScrollArea definite-height fix across remaining lists

Radix ScrollArea needs an explicit height, not max-height, or the
viewport collapses and clipped rows become unreachable. Extend the
Resources fix to security, settings, git, and create/import surfaces,
and drop redundant outer wrappers where ModalBody already scrolls.

* fix: migrate Networking tables to Radix ScrollArea

Networks and Findings used native max-h + overflow-auto, which worked
but broke glass scrollbar consistency with Resources and the design
system. Switch them to ScrollArea with a definite height and the same
mobile fallback as the other inventory tables.

* fix: warn Classic bar users that the style is retiring soon

When Appearance Navigation is set to Classic bar, show the same warn
SettingsCallout pattern used for Constrained graphics. Preference is
kept until removal; no alternate style is named in the copy.

* fix: move Channels delivery retries below channel tabs

Put channel configuration first and keep Delivery retries as a shared
footer control under the Discord/Slack/Webhook/Apprise tabs.

* fix: drop redundant More masthead from Smart bar overflow menu

The trigger already reads More, so the dropdown masthead repeated the
same label. Leave titled mastheads on Compact Navigate and Add quick
link menus.

* test: align Smart More E2E with masthead removal

The overflow menu no longer shows a More heading. Assert the menu via
the Logs item and lock that the redundant masthead stays gone.

* fix: consolidate Fleet Map toolbar filters into a single row

Adopt the same retractable search control used on Fleet > Overview and
move the flag filters (missing deps, port conflicts, orphans, shared)
onto the toolbar row right after the Graph/List selector. The node
filter becomes a dropdown instead of individual toggle chips so it
does not clutter the row as fleet size grows.

* fix: move Networking Topology filters onto the search toolbar row

Merge the ownership selector and boolean filter chips (include system,
exposed, drift, missing external, shared) onto the same row as the
stack/network search inputs, matching the Fleet Map toolbar layout.

* fix: default the reclaimable-space banner off

Resources > Docker & Storage's "Show reclaimable-space banner" toggle
now defaults to off instead of on. Also flips the /settings fetch
failure path to fail closed (hide the banner) to match the new
default, instead of failing open.

* fix: raise Compact launcher quick links cap from 5 to 7

* fix: add Discord link to Settings Support Self-serve

Gives users a community chat channel alongside Documentation and
GitHub Issues, using the official Discord mark since lucide-react
has no brand icon for it.

* fix: stop container NET I/O metric row height jump

Give NET I/O more column share than CPU/MEM and keep metric
values on one line with truncate so three-digit rates cannot
grow the strip.

* fix: elevate Doctor tab between Activity and Drift

Make Compose Doctor easier to find in the anatomy strip by placing
it with the ops judgment cluster, ahead of Dossier and inventory tabs.
2026-07-26 03:25:07 -04:00
Anso 719180f156 fix(fleet): verify update status before removing readiness cards (#1697)
* fix(fleet): verify update status before removing readiness cards

Full-stack Apply now rechecks persisted status after the health gate starts, reloads the live preview before dropping a card, and invalidates the hub fleet aggregation so cleared updates cannot resurrect from a stale cache.

Closes #1686

* fix(fleet): align persisted update status with preview semver detection

Share digest-plus-tag detection so post-Apply sidebar status matches Fleet and Anatomy.

* fix(fleet): keep tag-only updates advisory for Compose automation

Expose digestUpdate vs tagUpdate from checkImage so scheduled and API auto-update only apply same-tag digest drift Compose can pull.

* docs: clarify scheduled auto-update applies digest drift only

Document that higher pinned tags stay advisory until Compose is changed, matching schedule and Run Now behavior.

* docs: require Compose pin edits for higher-tag advisories

Stop recommending Apply now or Update as remedies that cannot rewrite a pinned image tag.

* docs: clarify Apply now pulls pinned tags only

Align the detection-cadence bullet with digest-rebuild vs higher-tag guidance.

* fix(fleet): keep tag advisories after apply and scheduled updates

Tag-only previews were treated as cleared on Fleet reload, and scheduled/
Run Now paths wiped status without rechecking. Align post-update verification
with the manual Apply path (health gate first, recheck, no blind clear) and
block digest apply when sibling image checks failed.

* fix(fleet): clear eslint unused-arg and containers assignment
2026-07-26 03:09:21 -04:00
Anso 0daddfde00 fix: reconcile sticky update indicators with Anatomy preview (#1698)
* fix: reconcile sticky update indicators with Anatomy preview

Sidebar, Updates filter, and Fleet treated retained partial/failed
scanner has_update as confirmed. Keep raw state for retention/notifications,
project confirmed-only to APIs, show distinct incomplete indicators, and
clear sticky rows only after an authoritative-negative preview.

Closes #1685

* test: align sidebar truncate E2E with failed-over-retained precedence

Purple update indicators are confirmed-only; hasUpdate with a failed
check correctly shows the failed trailing icon.

* fix: clear confirmed update rows on authoritative-negative preview

Address audit SF-1/SF-2/SF-3: observation-watermark clears for older
ok+has_update rows (DB + memory gens), Fleet checkability parity with
backend not_checkable, and Updates chip confirmed-only regressions.

* fix: tombstone equal-generation writers on preview clear

Advance the per-stack write generation when clearing at the observation
watermark so a scanner reserved before preview cannot recreate the row
after an authoritative-negative reconcile.

* fix: clear sticky updates with digest and tag preview parity

Share detection across scanner and preview, keep GET read-only with POST reconcile, gate Apply to digest and rebuild updates, and invalidate the hub fleet cache on clear.

* test: set digestUpdate on auto-update checkImage mocks

Scheduler and execute routes now gate Compose on digest drift; fixtures that expect an apply need digestUpdate so they exercise the update path.

* fix: clear unused lint errors on sticky update branch

Drop unused partial helper and fleet invalidate import; keep the CacheService inflight self-ref as let with an eslint exception so tsc stays green.

* fix: use inflight holder for CacheService prefer-const

Keep generation-aware ownership without a let self-reference that fights ESLint and tsc.
2026-07-25 15:42:19 -04:00
Anso 4d2270a097 fix(ui): use Docker health status terms on container cards (#1696)
Per-container metadata showed healthcheck passing/failing instead of Docker's healthy/unhealthy/starting, which disagreed with the stack pill. Render the normalized healthStatus token directly and document the same terms.

Closes #1677
2026-07-24 22:48:53 -04:00
Anso 17a8dc8a94 fix(blueprints): fail closed on marker ownership for apply and withdraw (#1694)
* fix(blueprints): fail closed on marker ownership for apply and withdraw

Require a matching .blueprint.json under the stack lock, persist required_blueprint_id on deletion intents, remove the legacy remote apply fallback, and protect the marker in the file explorer.

* fix(blueprints): add CodeQL path barriers on ownership probes

Use the canonical resolve-and-startsWith sanitizer inline at the marker and stack-directory fs sinks so js/path-injection clears.

* fix(blueprints): block delete on failed withdraw and defer marker write

Refuse Blueprint DELETE when pre-delete withdraw does not complete, and write .blueprint.json only after a successful deploy so failed applies cannot orphan stacks or claim an unapplied revision.

* test(blueprints): align lock-order assert with deferred marker write

Update the per-stack lock ordering expectations to compose, cleanup, deploy, then marker after the partial-apply fix.

* fix(deps): bump postcss past GHSA-r28c-9q8g-f849 for npm audit

Raise the Vitest/Vite transitive postcss to 8.5.23 so Backend CI audit --audit-level=high passes.
2026-07-24 15:57:18 -04:00
Anso 79914fe750 fix: recognize clean one-shot completions in health gate and drift (#1691)
* fix: recognize clean one-shot completions in health gate and drift

Treat exit 0 with restart policy no/absent as successful completion so
init and migration jobs no longer fail post-update observation or show as
service-missing, while long-running restart policies still fail closed.

* fix: ignore residual health on clean one-shots and honor deploy.restart_policy

Completed exit-0 jobs with no-restart intent no longer fail the health gate on leftover starting/unhealthy state, and Drift treats deploy.restart_policy with Compose precedence so any/on-failure services are not mistaken for one-shots.

* fix: require explicit Compose restart no for one-shot recognition

Docker inspect reports restart no for both intentional jobs and bare services that omit restart, so Health Gate and Drift now require declared restart:""no"" (or deploy.restart_policy condition none) and load Compose intent once per gate.
2026-07-24 09:41:21 -04:00
Anso 524cc56d2f feat: surface stack Monitor from header and service cards (#1693)
Make Alerts and Auto-heal reachable from the stack More menu and
container cards, with optional Compose service prefill in add forms.
2026-07-23 23:25:07 -04:00
Anso ec0f59a85e fix: dedupe healthcheck alerts and share crash rate limits (#1690)
* fix: dedupe healthcheck alerts and share crash rate limits

Health flaps no longer spam the bell: emit only on transition into unhealthy, keep a prune-surviving 60m dedup stamp that advances only when history persists, and share the fixed-window rate cap with crash alerts using typed roll-up copy. Docs now match fixed-window and non-persisted overflow behavior.

* fix: harden health alert rate refund and shutdown cleanup

Bind rate-token refunds to the issuing fixed window, clear deferred-retry markers on recovery/destroy/shutdown, and refuse deferred health dispatches after the service stops so the 20/min cap and no-duplicate guarantees hold under async races.
2026-07-23 23:14:25 -04:00
Anso a89498ae5b fix(ui): hide log service chips on single-service stacks (#1689)
Service chips only differentiate multi-service or multi-container log streams. Gate rendering with the same layout criterion already used in stack details, while keeping parsed prefixes and download attribution intact.
2026-07-23 20:22:57 -04:00
Anso 85842cc547 feat: add service-scoped stack alert rules (#1681)
* 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.
2026-07-23 17:57:04 -04:00
Anso dd54a2e483 feat: graduate Host Console to Community admins (#1669)
* 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.
2026-07-23 12:59:53 -04:00
Anso ed5ca9c4f6 fix(fleet): refresh prune reclaimable estimate after successful run (#1675)
* fix(fleet): refresh prune reclaimable estimate after successful run

The live estimate only re-ran when targets or scope changed, so the
toolbar and per-node reclaimable figures stayed stale after a real prune.
Invalidate via estimateEpoch when any non-dry-run target succeeds.

* fix(fleet): toast partial prune success honestly

A reachable node with mixed per-target outcomes made okNodes zero and
showed a total-failure toast even when some targets mutated Docker.
Gate the failure toast on zero successful targets anywhere.
2026-07-23 08:25:54 -04:00
Anso 698b7d0713 fix: purge deleted-stack notifications from panel and ticker (#1674)
* fix: purge deleted-stack notifications from panel and ticker

Stack delete already cascaded scans and drift but left notification_history rows, so the bell and Activity ticker kept showing the deleted stack. Purge those rows in the shared deletion lifecycle, invalidate connected clients, and drop node-scoped in-memory rows immediately.

* fix: target remote notification purge by hub node id

Remote stack-deleted invalidations always reconcile with hub rn.id, and
notification refetch preserves failed node slices instead of wiping them.
2026-07-22 20:00:45 -04:00
Anso a3edee5e6a feat: weekly UTC maintenance windows for mute rules (#1661)
* 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.
2026-07-21 23:17:52 -04:00
Anso e15b9d1244 fix(blueprints): write compose.yaml so first-time apply is not shadowed (#1668)
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.
2026-07-21 21:19:03 -04:00