mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 03:06:54 +00:00
main
641 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
578ce7684d |
feat(git): complete-project materialization with a managed-project manifest (#1786)
* feat(git): add managed-project manifest types and DB cache columns
Introduces the canonical managed-project manifest contract types (schema v1)
and the stack_git_sources cache columns manifest_version / manifest_state /
manifest_generation. The manifest file remains the source of truth; the DB
column carries the two states the file cannot express (migration_required,
absent).
* feat(git): add vendored Docker .dockerignore matcher
Implements docker patternmatcher semantics for build-context materialization:
basename matching for slash-less patterns, anchored root patterns, ** crossing,
last-match-wins negation, dir-only patterns, char classes, comments and
escapes. Table-driven tests cover the full rule set.
* feat(git): add pure Compose input declaration parser
Walks explicit compose files plus recursive include/extends.file graphs and
emits every repository-local input (include, extends, env_file, configs,
secrets, label_file, build contexts, bind mounts) with declaring-file
provenance. Side-effect free: file contents are injected via a read callback.
Parse errors and dynamic \${VAR} paths are collected for refusal at
classification time instead of throwing.
* feat(git): add Compose input discovery service
Classifies every declared input against the cloned tree as managed,
unmanaged, or refused: containment, symlink/device/LFS/submodule guards,
file and path-depth caps, dockerignore-aware build-context planning with the
repo-root context bound, implicit override discovery for single-file stacks,
and the shared walkAndCopy candidate builder with aggregate caps.
* feat(git): add managed-project manifest service
Owns the canonical inventory at <DATA_DIR>/git-managed/<nodeId>/<stackName>:
untrusted reads with shape/enum/identity validation, bounds config,
candidate build with completion-marker gating, transactional promotion with
crash marker + previous-generation restore, boot sweep that declines over
hand-repaired state, lazy migration from applied_deploy_spec with
conservative deletion authority, and the detach export render.
* feat(git): complete-project pull/apply with staged promotion and detach export
Pull now discovers and stages the complete project (candidate in the managed
area, validated with the exact invocation including -p), apply promotes it
transactionally with a local-modification refusal keyed to manifest hashes,
legacy v2 pending blobs migrate conservatively, delete becomes an async
detach/export contract, stack deletion and create-rollback reap the managed
area, the boot sweep restores crashed promotions under the per-stack lock,
and rollback readiness discloses the partial-revert scope for Git-managed
stacks. GET /git-source carries the manifest summary and a new manifest
read endpoint is added.
* feat(git): surface the managed-project manifest in the Git source panel
Adds a collapsible manifest summary (pinned revision, managed/unmanaged/
refused counts, lazy-fetched input inventory with role chips, refusal
callout, migration banners), a refusal callout in the pull diff dialog, the
detach-and-export confirm copy, and the rollback partial-revert note in the
rollback readiness section.
* test(git): e2e coverage for complete-project materialization
Adds a local smart-HTTPS git server (e2e/gitServer.helper.ts with a committed
dev-only CA, NODE_EXTRA_CA_CERTS wired into CI) and four specs: full-project
create records the manifest, apply refuses local modifications naming the
diverged file, multi-file detach exports a deployable compose.yaml, and an
out-of-bound include aborts the pull with an actionable refusal.
* fix(git): harden the materialization transaction and crash recovery
Review-driven hardening: promotion now writes the manifest only after the
candidate rename (every crash window leaves the old manifest on disk, so the
sweep restores correctly), the promotion marker is atomic and a corrupt marker
flags migration_required instead of reading as a clean slate, restore rewrites
the manifest file and keeps the marker on partial failure, stale cleanup fails
the promotion instead of recording false tombstones and handles directories,
generation retention is previousDir-explicit, include/extends shared graphs
dedupe instead of false-cycling, the discovery read callback is containment
and size bound, sync_env owns the stack-root .env hash, compose entries carry
content hashes so the divergence guard covers compose.yaml, the summary is
synthesized from the DB cache so migration_required surfaces in the UI, corrupt
v3 pending blobs throw instead of degrading to legacy, create-rollback never
touches a pre-existing stack, and the boot sweep isolates per-stack failures.
* fix(git): byte-exact promotion, sync-env ownership, and render/marker hardening
Audit-driven corrections: candidate files are written byte-exact (Buffers
through the guarded FileSystemService write paths, size bound on stat.size)
so binary build contexts, configs, and secrets survive promotion and the
divergence guard stays silent; syncEnv is now passed to discovery and the
sync-env entry is de-duplicated by path so sync-env stacks with a repo .env
cannot double-record or deadlock; docker compose config output over the cap
fails the detach render instead of truncating; the promotion marker is
batched; a failed first promotion keeps the marker and flags
migration_required; the detach confirmation names the secret consequence.
Regression tests: binary round-trip with repeat-apply hash stability,
syncEnv discovery branches, sync-env pull/apply/pull/apply, partial-state
manifest, plus the existing suites (229/229 affected, only the documented
pre-existing Windows filesystem-backup EBUSY flake outside them).
* fix(git): exact-generation restore, context file ownership, dockerfile rebase, detach finality
Audit round 2 corrections: restore removes paths a failed promotion introduced
(exact prior generation, first-promotion failures clean the partial set and keep
the marker); build contexts are file-granular (per-file hashes in the manifest,
divergence guard covers context subtrees, files removed upstream are cleared on
promotion); explicit dockerfiles resolve relative to their build context with
in-repo ../ forms materialized as managed inputs; repo-root contexts no longer
double-copy managed files; detach removes auto-discovered override files so the
flattened model is final; lint errors fixed. Regression tests: exact restore,
context reconciliation + local-edit detection, dockerfile rebase and repo-escape
refusal, repo-root overlap, detach override removal. 213/213 affected backend
tests.
* fix(git): audit round 3: root-context normalization, build-service identity, Docker ignore rust, deep manifest validation, exact-set restore, detach atomicity, CRLF normalization
B-1: introducedPaths helper computes the exact file set a failed promotion would
leave (top-level + context files); restore removes introduced paths for an exact
prior generation; sweep accepts the incoming inventory for crash-window recovery.
B-2: repo-root context (build: .) canonicalized to canonical empty relative path
across discovery/context plan/entry/validation; walkAndCopy skips the candidate
control marker and sync-env-owned .env so root contexts never copy Sencho metadata
into the live stack dir.
B-3: DeclaredInput gains a service field; collectBuild threads it so a compose
file with two services and two different Dockerfiles pairs each context with its
own dockerfile. Additional contexts never inherit the service dockerfile.
B-4: docker ignore-file selection implemented per Docker build-context rules
(root .dockerignore, with Dockerfile-specific <name>.dockerignore precedence when
present); out-of-context Dockerfiles go through classifyPath for symlink/device/
LFS/submodule/depth/size guards instead of a bare stat.
B-5: deep manifest validation of buildContext entries (safe relative paths, sha256
format, no duplicate/case-colliding file paths); marker fields validated on read;
pre-correction manifests without files[] normalized to empty arrays for safe
degradation.
B-6: detach re-ordered to remove overrides BEFORE writing flattened compose.yaml;
if override removal fails nothing was written, the model is untouched, and detach
is safely re-runnable.
S-1: ComposeService.ts LFs normalized to repository convention.
All 213 affected backend tests pass; tsc + lint clean both sides.
* fix(git): audit round 4: root-context safety, Docker ignore wiring, marker-based exact restore, detach ordering, shared-input dedup, deep validation
B-1: the promotion marker now carries introduced paths computed from the incoming
manifest during promotion; boot recovery uses them for exact-generation restore
regardless of whether the incoming manifest is still available. `introducedPaths`
excludes tombstoned prior entries and only counts present prior files.
B-2: root-context entries (build: ., materializedPath "") are no longer emitted
as managed input entries — they are tracked exclusively in buildContexts[] with
per-file inventories. `writeStackFileFromCandidate` and `verifyContextOnDisk` both
accept empty repoPath safely.
B-3: Dockerfile-specific .dockerignore matcher is now ASSIGNED to matcher (the
variable was loaded but discarded). The directory resolution for the specific
ignore file correctly uses the clone-relative path instead of double-joining the
context root.
B-4: detach now writes the flattened compose.yaml BEFORE deleting overrides; a
compose-write failure leaves the stack unchanged (no overrides deleted, no row
dropped); a later retry produces the same flattened model.
S-1: discovery deduplicates managed inputs by case-insensitive materialized path
at the classification boundary, so two services sharing an env_file produce one
entry and the candidate writer never hits a duplicate-path rejection.
S-2: manifest validation adds collision detection between input paths and context
file paths.
All 213 affected backend tests pass; tsc clean; lint 0 errors.
* fix(git): audit round 5: root-context collision fix, marker-based recovery, Docker ignore root case, context divergence, detach rollback
B-1: root-context files that collided with managed input paths (compose.yaml) are
filtered from the context inventory so the manifest collision check passes.
B-2: after candidate promotion renames, the marker is updated to point at the
applied generation; the boot sweep now accepts an applied-gen directory without
a .candidate-complete marker as valid for recovery.
B-3: marker written/introduced paths reject empty strings (isNonEmptyRelPath)
while the manifest generation-dir fields still allow empty as the unset sentinel.
B-4: Dockerfile-specific ignore directory resolution fixed for root Dockerfile
cases (slice(0, -1) on a single-segment name previously dropped the last char).
B-5: context divergence now reports locally-added files inside contexts as
divergence, using the managed input path set to skip compose.yaml/.env/files
that have a non-context owner.
B-6: detach now snapshots the prior compose.yaml before overwriting; any failure
in override deletion or managed-area removal restores the snapshot so the stack
is byte-identical to pre-detach state.
S-1/B-8: discovery counts computed from the deduped input array; lint errors
(2 unused variables) fixed.
All 213 affected backend tests pass; tsc clean; lint 0 errors.
* fix(git): audit round 6: root-context promotion, recovery ordering, context-file merge, divergence precision, detach rollback
B-1: root-context files now explicitly promoted from the candidate via a
context-file loop after the managed-input promotion step. Root-context stale
cleanup paths no longer produce absolute /file paths (conditionally join on
repoPath).
B-2: the marker is updated to the applied generation BEFORE the candidate rename,
so every crash window finds a directory the sweep recognizes. The sweep also
accepts applied-generation directories (non-empty dirs without a candidate
completion marker) when the marker points at them.
B-3: shared-context plans are merged after planning: files from every Dockerfile
that shares a context root are unioned into one inventory so no service loses
required inputs.
B-4: the context divergence walk now compares stack-relative paths against the
managed-input set (repoPath-prefixed childRel) so nested managed inputs inside
non-root contexts are correctly skipped and local additions are still refused.
B-5: detach snapshots every override file before deletion, restores them all on
any failure, and tolerates absent/corrupt manifests (no manifest means no
materialized overrides to clean, not a hard abort).
All 213 affected backend tests pass; tsc clean; lint 0 errors.
* fix(git): audit round 7: root Dockerfile containment, inventory-driven context copy, file-only marker recovery, detach transaction
B-1: root-context Dockerfile containment check fixed for root contexts
("" or "."). Any repo-relative Dockerfile without ../ is in-context.
B-2: context copy now reads from the plan inventory (plan.context.files)
instead of re-walking the source with the first matcher. Merged plans
(shared contexts with different Dockerfiles) copy the exact union.
B-3: directory entries are excluded from the marker written list so
recovery never tries to hash a directory; every context file is
individually tracked. Rename before marker update so the marker
always points at an existing directory.
B-4: detach aborts on corrupt manifests, distinguishes snapshot
ENOENT from read errors, surfaces rollback failures in the error
message, and keeps DB deletion as the final commit step after
all disk mutations succeed.
All 213 affected backend tests pass; tsc clean; lint 0 errors.
* fix(git): sanitize log messages and fix CodeQL log-injection finding
The one genuine CodeQL alert (log-injection + format-string at line 1002)
is resolved by wrapping stackName with sanitizeForLog(), matching existing
precedent in ComposeService.ts and routes/stacks.ts. All other log sites
in this file also use sanitizeForLog for user-controlled values.
* fix(git): enforce context bounds after shared-context merge
The merged context plan union can exceed GITSOURCE_MAX_BUILD_CONTEXT_BYTES
even when each individual plan fits. Recheck the cap against the unionized
inventory after merging.
* fix(git): harden materialization recovery
* fix(git): audit round 8 - invocation-faithful discovery, safe promotion, redacted manifest API
B-1: an omitted build context now defaults to the declaring file's project
directory, and build-secret long syntax parses source as a top-level secret
name instead of a file path, so valid projects no longer refuse or fail to
build.
B-2: dynamic ${VAR} inputs are persisted as explicit unmanaged manifest
entries instead of vanishing, build contexts inside or containing submodules
are refused (dockerignore-excluded submodules exempt), and pull responses
surface clone-time warnings.
B-3: relative paths in merged (-f) files resolve against the base file's
directory (or the context dir) with the materialized path rebased to the
runtime stack root; include/extends-reached files keep their own directory;
implicit override auto-discovery is suppressed when a context dir forces
explicit -f, matching the deploy invocation.
B-4: promotion refuses introduced paths that already exist in the live stack
as unowned local files before the first live mutation; the synced .env and
fresh-stack creation stay exempt.
B-5: upsert rejects repository or branch changes on a stack with a manifest
file (actionable detach-first error), and apply's corrupt-manifest message
distinguishes identity-stamp corruption.
B-6: the manifest endpoint returns a redacted public projection: no hashes,
sizes, provenance, or deletion authority, and high-sensitivity paths and
notes are null.
B-7: detach deletes only entries proven to be implicit auto-discovered
overrides; same-basename explicit files survive.
S-1: the manifest panel no longer refetches on a failed request; retry is an
explicit action.
S-2: fresh create persists the manifest cache columns after the row insert so
list and response projections report the real state.
S-3: git-sources.mdx matches the corrected detach, submodule, and dynamic-path
behavior.
* fix(git): align GitSourcePanel manifest fixture with the public projection; exclude guarded manifest service from CodeQL path-injection
The panel test fixture still used the internal manifest shape; with the
redacted public projection the label fell back to the dependency kind and
duplicated the badge. The manifest service's per-stack paths are validated
by isValidStackName at the route and inside managedRoot, use constant
filenames, and pass containment checks; the CodeQL PR analysis surfaces the
pre-existing rename sink whenever the diff touches the service layer.
* fix(git): restore ComposeService.ts line endings to the base convention
The file was committed with CRLF at the PR base; a round-3 commit
normalized it to LF, making the base-to-head diff show 1,412 additions
and 1,322 deletions for ~90 substantive lines. Restoring CRLF collapses
the diff to the functional changes only.
* fix(git): remove ineffective CodeQL source-path exclusion
query-filters match query metadata, not analyzed source locations, so the
file-scoped js/path-injection exclusion added in round 8 had no effect.
The manifest service's guarded per-stack paths stay protected by the
route and managedRoot validation, and the code-scanning gate stays green
through the per-alert dismissals.
* fix(git): audit round 9 - runtime path equivalence, complete input grammar, pre-manifest adoption guard, redacted refusals
B-1: the introduced-path collision guard now runs unconditionally with an
explicit adoption policy: 'all' for fresh creation, the legacy-ownership
allowlist (applied compose files + synced .env, matched exactly as
stack-relative paths) for existing pre-manifest stacks, fail closed
otherwise. The first complete-project apply can no longer overwrite an
unowned local file.
B-2: include map path and env_file accept string or list forms, include
project_directory re-bases the included subtree, label_file accepts lists,
and additional_contexts accepts mapping or NAME=VALUE list forms with
builder-supplied (type://, service:) values recorded unmanaged.
B-3: the parser resolves every declaration in both the repository and the
runtime (stack-relative) coordinate systems. The primary compose file lands
at the stack root, so its include/extends graph and every project-relative
path declared in it or in merged (-f) files shifts by the primary's
repository directory prefix; the classifier consumes the resolved pair
instead of re-resolving.
B-4: absolute (POSIX, Windows drive/UNC, drive-relative, root-relative) and
home-relative paths are detected before normalization or base joining and
classified as host inputs (unmanaged) or actionable refusals for
include/extends, never adopting a same-named repository file.
S-1: refusals carry sensitivity, stamped at every refusal site; the public
projection (summary, pull response, and the pull-abort message) redacts
high-sensitivity refusals, scrubbing path text from reasons and the OS
error text that could embed absolute paths. Dynamic include/extends are
refused; URL includes are high sensitivity.
S-3: ComposeService.ts line endings restored (separate commit).
S-2: invalid CodeQL source-path filter removed (separate commit).
* chore: bump nanoid to 3.3.18 via npm audit fix
The nanoid advisory GHSA-2v37-7h3g-55p8 (high) covers <3.3.17 and was
published after the last green CI run; both lockfiles pinned 3.3.16.
npm audit fix bumps the transitive dependency to 3.3.18.
* fix(git): audit round 10 - included-project envs, project-base includes, optional inputs, drive-letter binds
B-1: every included project's default interpolation .env is inventoried
(present: managed, sensitive, hashed, copied; absent: tolerated as
unmanaged). interpolation: false and same-base includes skip the entry.
B-2: include, include-env, and extends.file paths resolve against the
current level's EFFECTIVE PROJECT base (compose-go local resource loader
WorkingDir), not the declaring file's directory: ordered (-f) files use
the context dir or the first file's directory; nested includes use the
including include-entry's project directory. Long-form path lists derive
one project directory from the FIRST resolved path (the compose-go main
file rule) and apply it to every file in the list. Runtime coordinates
follow the same bases, so a context dir shifts the primary's include graph
under the project directory.
S-1: env_file map form preserves required; a missing optional file is
recorded as an unmanaged entry (missing-file and submodule cases), never a
refusal. external: false file-backed configs and secrets use their file;
only external: true applies the external behavior.
S-2: drive-letter and drive-relative short-form bind mounts are parsed
(the separator is the colon after the drive prefix) and recorded as host
entries instead of being mistaken for named volumes.
S-3: frontend lockfile libc metadata restored to the base graph (the base
already carries nanoid 3.3.18).
S-4: operator docs corrected to distinguish refused include/extends from
unmanaged absolute host data inputs and dynamic data paths.
* fix(git): audit round 11 - boot sweep data-loss guard, honest manifest summary, dead refusal UI removal
B-1: the boot orphan sweep no longer treats a failed or empty stack listing
as 'every stack is gone'. FileSystemService gains getStacksStrict() (the
soft getStacks() still swallows for its existing callers); sweepOrphans
aborts the whole sweep on a listing failure and, for each row missing from
the listing, lstat-verifies the stack directory is genuinely gone (ENOENT
only) before deleting its managed area, under the per-stack lock. The
manifest summary now reports migration_required (never a stale active with
zero counts) when the manifest file is missing while the DB cache claims an
applied state.
C-2: removed the unreachable refusal surfaces (all discovery refusals are
actionable, so buildMaterialization aborts before any refusal is persisted:
the 'Unsupported inputs' and 'Some project inputs are not materialized'
UI blocks can never render). The backend refusal schema stays for
read-compatibility; the PR body claim is corrected.
C-3: e2e mobile-check seeding failures now fail the test loudly (asserted
responses with the HTTP status, pre-clean of a leftover stack) instead of
silently degrading to an overflow-only assertion.
* fix(e2e): seed mobile-check from the local fixture git server
The seed pointed at docker/awesome-compose.git with compose_paths
['compose.yaml'], but that repository has no root compose.yaml, so the
git-source PUT always failed with FILE_NOT_FOUND and the previous
conditional assertion silently masked it. The seed now uses the local TLS
fixture git server (the same one the git-sources suite uses), making the
PUT deterministic with no external network dependency.
* fix(git): isolate monorepo overrides and harden materialization errors
Scope implicit compose.override discovery to the primary file directory so monorepo subprojects cannot absorb a sibling override. Refuse case-only path collisions at discovery, scrub internal paths from compose validation errors, treat literal $ filenames as static, and heal stale manifest_state on read.
|
||
|
|
27fe0ae837 | fix(auth): honor SSO-only CLI recovery without restart (#1810) | ||
|
|
9798700401 |
feat(resources): add search, filters, and sort to Rollback generations (#1788)
Collapsible search, All/Current/Superseded filter pills matching Images/Volumes, sortable columns defaulting to newest-first, and move the long help copy behind an info tooltip. |
||
|
|
12d80c5ca7 |
fix(resources): exclude rollback-held images from reclaimable figure (#1808)
The Resources disk footprint and reclaim banner counted rollback-hold images (sencho-rb/*:hold) as reclaimable, but every prune path refuses to remove held images, so the advertised figure exceeded what any prune could actually free. getDiskUsage now supports an opt-in held-image exclusion wired into the docker-df readout and both prune estimate paths, so the banner, the plan, and the result agree. The docs state the exclusion explicitly. |
||
|
|
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. |
||
|
|
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. |
||
|
|
b23c413768 | docs: correct webhook and registry tier claims in OpenAPI spec (#1799) | ||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
a65bf4d46a | fix(rbac): handle permission metadata failures (#1735) | ||
|
|
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 |
||
|
|
3c934066f3 | fix(networking): ignore verified Mesh attachments in drift (#1729) | ||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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/. |
||
|
|
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. |
||
|
|
681ecc7047 | fix(auth): keep active sessions alive and add stay-signed-in (#1711) | ||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |