docs: v1 docs refresh (batch 7) (#1627)

* docs(introduction): refresh screenshots and correct stale nav/tab coverage

Replace all 5 screenshots with fresh 1920x1080 production captures.
Document the shipped Take down stack action, the Compose Labels stack
tab, and the Docker Labels Fleet tab, none of which were mentioned.
Correct the Console nav item to note it is a limited-availability
surface rather than a plain role/tier-gated view.

* docs(quickstart): refresh screenshots and correct preflight, dashboard, and menu drift

Replace all three quickstart screenshots with fresh captures and correct
several claims that drifted from the current UI:

- Document the environment preflight's 7th check (Sencho compose
  location), previously missing entirely from both the text and the
  screenshot alt text.
- Add the Stack health table's Source and Port columns, and note that
  columns are sortable and default to load order.
- Note the masthead's running-container count, live CPU/memory readout,
  and alert count.
- Fix the profile menu list: replace the nonexistent "Feedback" entry
  with "Open New Issue", drop "Appearance" (it lives in Settings, not
  the profile menu), and add the conditional "Billing" entry.
- Note that the sidebar groups stacks by Docker Compose label once
  labels are assigned, with pinned stacks first.

* docs(configuration): document missing env vars and fix JWT secret wording

Add SENCHO_UPLOAD_DIR, TRIVY_CACHE_DIR, SENCHO_MESH_RECONCILE_INTERVAL_MS,
SENCHO_MESH_PROXY_TUNNEL_IDLE_MS, SENCHO_COMPOSE_COMMAND_TIMEOUT_MS, and
SSO_OIDC_CUSTOM_ENABLED, all real environment variables that were missing
from the reference tables. Clarify that the JWT signing secret has no
environment variable at all, generated and stored in the database only,
rather than implying an unused JWT_SECRET var exists.

* docs(stack-management): refresh against current live app and source

Rewrites the Stack Management page against the production node and
frontend source: adds the Compose Labels anatomy tab (new since the
last refresh), the Mute action on the stack header and sidebar
context menu, corrects the update-available banner wording and the
sidebar context menu's lifecycle ordering, notes the Doctor tab's
severity dot and the scan-status banner, and replaces all 14
screenshots with fresh production captures.

* docs(editor): refresh against current live app and source

Replace all 8 screenshots with fresh 1920x1080 production captures (mobile
shot at phone viewport). Document the self-stack protection dialog that
blocks deploy/delete actions on the stack running the current Sencho
instance, the multi-container summary strip and Compact/Detailed density
toggle, the mutually exclusive Expand containers / Expand logs controls,
the Files tab full-screen toggle, and the post-deploy scan-status banner
on the Anatomy panel, none of which were previously covered.

* docs(editor): recapture mobile compose screenshot without redaction

The previous mobile screenshot used the plex stack, whose compose volume
mount includes a real host path that had to be blacked out and overlaid
with placeholder text, leaving a visible seam. Recapture against the
dozzle stack instead, whose only volume is the Docker socket, so the
screenshot needs no editing.

* docs(stack-file-explorer): refresh page against current app and code

Renames "Files & Volumes tab" references to the current "Files" tab
label, documents the full-screen toggle and word-wrap control, expands
the non-browsable-volume reasons to match the current containment
logic (single-file binds, the full protected host-path list, and the
Docker-unreachable named-volume case), documents the 100-item bulk
selection cap and the 5000-file/1 GiB archive limits, corrects the
non-existent DISK_FULL error code, notes that override compose files
are unprotected, and scopes the atomic-write claim to fs-backed roots.
Replaces all 9 screenshots with fresh production captures taken in the
Files tab's full-screen mode, so no other stack UI (health metrics,
logs) appears in the background.

* docs(resources): refresh against current live app and source

* docs(dashboard): refresh against current live app and source

* docs(app-store): refresh against current live app and source

Replaces all 6 screenshots with fresh production captures and corrects
the Environment Variables and About-panel metadata claims to match what
the bundled LinuxServer.io registry actually returns today.

* fix(docs): recapture app-store advanced-tab screenshot without scroll cut-off

The prior capture was taken mid-scroll to reveal the Security checkbox,
leaving the template logo and About text cropped awkwardly at the top.

* docs(global-search): refresh against current live app and source

* docs(deep-links): refresh against current live app and source

- Add App Store, Logs, Update, Console, and Audit to the URL table (previously undocumented)
- Document hub-only URL redirect behavior and cross-link to Multi-Node Fleet
- Note Fleet tab URL segments do not always match their on-screen label (Status/configuration, Map/dependencies)
- Document the no-env-files edge case (Env tab absent, falls back to compose)
- Clarify Settings section-list state is phone-only; desktop always normalizes to /settings/appearance
- Note which top-level views share identical URLs between desktop and phone
- Remove greenfield-violating temporal phrasing ("now has", "as today")
- Replace em dashes in touched bullet list

* docs(appearance): refresh against current live app and source

* docs(stack-activity): refresh against current live app and source

Add the missing "Stack taken down" event category, cross-link the
Suppressed badge to notification mute rules, list the new Compose
Labels tab in the Anatomy panel strip, and replace both screenshots
with current production captures.

* docs(dossier): fix generated-facts wording, add rollback readiness section

Corrects three Dossier tab Generated Facts rows against current frontend
behavior: the Ports count is not filtered to host-published mappings, the
Network row's "bridge" label is fixed text rather than a derived driver,
and missing env var names appear only in the Markdown export, not the
live tab. Adds a Rollback Readiness subsection under Connected Features
(previously only an orphaned Limitations bullet), links to Compose
Networking and Storage Portability, and clarifies that a missing export
section can mean either an older node build or a failed on-demand fetch,
not only a version gap. Recaptures all three screenshots against
production.

* docs(stack-drift): refresh against current live app and source

Fixes the stack detail tab bar description (Compose Labels tab was
missing, Files/Edit compose were wrongly listed as tabs), documents
that re-check only requires read access to the stack (so viewer and
auditor roles can trigger a ledger write), and cross-links the network
findings to the Networking tab's runtime drift section and the Fleet
Overview's Drift filter. Replaces all four screenshots with fresh
1920x1080 production captures.

* docs(compose-doctor): refresh against current live app and source

Corrects the rule count from 31 to 32 and documents the previously
undocumented self-managed-stack guard rule and enforcement, the
per-browser dismiss control for the summary card and Doctor tab dot,
the acknowledged summary state, the full Health-Gated Updates verdict
mapping, and the temporary exposure intent option. Replaces all four
screenshots with production captures.

* docs(compose-networking): refresh against current live app and source

Adds cross-links to the new node-wide Networking operator page, documents
the "view node networking" link and the unset/inherit exposure-intent pill
labels, corrects the stale Resources Hub network-tab reference, and
replaces all screenshots against the production node.

* fix(docs): unbreak stack-activity page render

The screenshot alt text used backslash-escaped quotes, which is
invalid MDX/HTML attribute syntax. Mintlify failed to parse the file
and silently dropped it from routing, so the page 404'd despite being
listed in docs.json navigation.

* docs(environment-guardrails): refresh against current live app and source

Fix the ENV FILES section description: it lists env_file: entries and
project files with an existence problem, not every declared env file.
A cleanly-resolving project file is already named in the Project
Environment File panel above it. Replace all three screenshots with
current production captures.

* docs(docker-label-audit): refresh against current live app and source

Add production screenshots (page had none), a Capability gating
section, a Limitations section, and a Troubleshooting accordion.
Tighten the secret-redaction heuristic description and cross-link
Stack Labels, Compose Doctor, Environment Guardrails, and Node
Compatibility.

* docs(compose-storage): refresh against current live app and source

Captured fresh production screenshots (both prior images were stale)
and fixed a leftover pre-rename card title pointing at the Files tab.

* docs(stack-labels): refresh against current live app and source

Removed the stale trailing-dot sidebar claim (that rendering was
removed from the sidebar in a prior UI pass), documented the new
label-scoped notification muting available from the sidebar, stack
context menu, and Settings panel, corrected the Fleet Actions card
copy to match the redesigned cards, added the previously undocumented
bulk-assign size cap and the node-scoped label bulk-action API, and
replaced all 8 screenshots with fresh production captures.

* docs(sidebar): refresh against current live app and source

Removes the stale per-row label dot claim (removed from StackRow), corrects the update indicator and Updates chip color from orange to fuchsia, and documents Mute/Take down in the context menu, the label group mute kebab, Ctrl+A/Esc bulk-mode shortcuts, pin-eviction toast, and the offline-node skip behavior in cross-node search. Replaces all 8 screenshots with fresh production captures.

* docs(deploy-progress): refresh against current live app and source

Corrected the Scan entry point (node-wide Scan this node from Security
Overview, not a per-stack config scan), added the missing Take down
verb and entry point, broadened recovery actions to cover restart and
rollback failures, fixed the modal status text and raw-output color
claims to match the live UI, and replaced all screenshots with fresh
captures from a throwaway demo stack on the production node.

* docs(atomic-deployments): refresh against current live app and source

* docs(health-gated-updates): refresh against current live app and source

* docs(deploy-enforcement): refresh against current live app and source

Correct the tier note to every-tier (verified against the current
policy gate, which no longer has a paid-only blocking switch), add the
Security Overview deploy-enforcement summary card, tighten the honor-
suppressions default wording, cross-link the separate pre-deploy scan
advisory dialog to avoid confusion with the block dialog, and replace
all three screenshots against the current production UI.

* docs(blueprint-model): refresh against current live app and source

Replaced all 11 screenshots with fresh production captures. Corrected
the volume-destroying-drift Enforce-downgrade claim (appeared three
times): that code path has no call sites in the runtime reconciler, so
any compose edit on a stateful or state-unknown blueprint always
re-enters Awaiting confirmation regardless of drift mode. Corrected
the Create workflow (creation does not trigger an immediate
reconciliation tick) and the Delete workflow (now requires typing the
blueprint name to confirm).

* docs(scheduled-operations): refresh against current live app and source

* docs(blueprint-model): drop tier framing, treat as Community-native

Blueprints carry no tier gate, so "available on every tier" implied a
comparison that doesn't exist. Removed the tier framing from the intro
note, the Prerequisites table, and the Security section; the role
requirement (admin to write, every role to read) already says
everything that matters.

* docs(auto-update-policies): refresh against current live app and source

Documents that every update trigger on this page, Apply now and
scheduled Auto-update tasks alike, runs through the same atomic backup,
build-aware rebuild, and post-update health gate as a manual update
from the stack editor, and is subject to the same deploy enforcement
policy gate. Corrects the readiness-computation description to scope it
to registry-image services and cross-links to Health-Gated Updates,
Atomic Deployments, Deploy Enforcement, and Stack Management. Replaces
the readiness board screenshot with a fresh production capture and
removes three orphaned screenshots left over from the page's prior
CRUD-policy layout.

* docs(auto-heal-policies): refresh against current live app and source

Correct the admin-only prerequisite: viewing policies and history is
open to every signed-in role, only creating/toggling/deleting requires
admin. Rewrite the matching troubleshooting entry to match, note the
overlap behavior between an All-services and a named-service policy on
the same container, and replace all three screenshots with a real
production policy.

* docs(webhooks): refresh against current live app and source

Replaced all three screenshots with fresh production captures (single
webhook create/reveal flow, two-card configured list), verified every
claim against WebhookService/StackOpLockService/registry.ts, and added
the previously undocumented per-stack lock skip behavior for
non-Git-source-sync actions with a matching troubleshooting entry.

* docs(global-observability): refresh against current live app and source

* docs(audit-log): refresh against current live app and source

- Fix nav terminology: Audit is a top navigation tab, not a sidebar tab.
- Correct retention claim: Admiral shows full retained history (default
  90 days), not a fixed 14-day window; the 14-day clamp only applies to
  the Community API, which has no navigation entry point at all.
- Fix settings path: Settings - Operations - Data Retention (previously
  pointed at the now-split Developer Diagnostics section).
- Restore current Recovery Vault naming (was stale Sencho Cloud Backup).
- Expand the tracked-actions list: stack take-down, label management,
  MFA reset, and notification suppression rules were missing.
- Note that assigning the Auditor role itself requires Admiral.
- Replace all four screenshots with fresh production captures (Stream,
  Table, expanded row, Data Retention) reflecting the current five-row
  retention card.

* fix(audit-log): reframe data-retention screenshot to card content only

The prior crop included the Settings sub-navigation panel, which isn't
part of what the surrounding text describes. Retake tightly cropped to
match the page's other screenshots (card content only, no chrome).

* docs(multi-node): refresh against current live app and source

Rewrites the Pilot Agent enrollment flow (now a generated compose.yaml
plus docker compose up -d, not a single docker run command) and the
Settings scope table (current registry: Stacks, Container Alerts,
Docker & Storage, Data Retention, Mute Rules, Recovery, and the
Admiral Account / Recovery Vault renames). Adds a Sencho Mesh
cross-link matching the live add-node form copy. Replaces all eight
screenshots with sanitized production captures.

* docs(pilot-agent): refresh against current live app and source

* docs(readme): refresh GitHub README against current live app

Replace all 9 screenshots (stale top nav showing a removed Console
item and missing the new Networking page); document Take down,
Drift Detection, Environment and Secrets Guardrails, Storage
Portability, Docker Label Audit, Remote Updates, and the node-wide
Networking dashboard; fix a broken non-root-user anchor link; correct
the notification channels list (no email channel exists yet) and the
global search scope (pages, nodes, and stacks, not containers or
services); rename "scan policy packs" to the current "scan policies"
terminology; broaden the App Store bullet to cover custom registries.

* docs(readme): reposition intro copy and refresh badge row

Lead with DevOps/platform/sysadmin framing instead of homelab-first,
drop pre-1.0 "production" language, state plainly that Sencho
provides a UI instead of promotional "does the work you do" phrasing,
and call out that multi-node was part of the architecture from the
start. Swap the Discussions badge for CodeQL, last-commit, open
issues, and live website/docs status badges, and add a blog link.

* docs: retake Dashboard and Global Search screenshots for accuracy

Configuration Status now shows Recovery Vault instead of the stale
Cloud Backup label, and the search palette capture includes the
Networking page entry added after the prior screenshot batch.

* docs: refresh Fleet View page against current app

Fleet View has drifted since its last rewrite: the Docker Labels tab,
Export Dossier action, Networking filter/badge, node label pills and
latency in topology, the Policy sync status row, and the node card
Mute submenu were all shipped but undocumented. The Check Updates and
Add Node actions also moved into the Overview toolbar (renamed Node
Update / Manage Nodes) and no longer sit in the shared action row.

Replaces all five screenshots with fresh production captures and
corrects the masthead's motion description (a calm shimmer on Healthy,
a steady glow on Degraded/Critical, not a pulsing dot). Also fixes a
Fleet Sync limitation that claimed no first-party sync-status panel
exists, now that the Status tab surfaces one.

* docs: refresh Fleet Dossier page against current app

Adds the page's first screenshot, documents the network exposure summary now included per stack, documents the storage-summary omission versus the Stack Dossier export, and adds two troubleshooting entries.

* docs(fleet-federation): deep rewrite for the rollout-approval gate

Federation's cordon and pin controls no longer take effect by
themselves: the reconciler requires a confirmed rollout preview
(Apply now -> Confirm Apply) before it mutates the fleet, and pinning
or unpinning always clears a blueprint's approval. Rewrote the mental
model, step-by-step, lifecycle table, security section, limitations,
practical workflows, troubleshooting, and cross-links to reflect that
gate. Also corrected the audit-log claims: cordon/uncordon/pin are not
filterable by a node.cordon/blueprint.pin action taxonomy, only by the
free-text search box against the method, path, and summary.

Replaced 4 screenshots and added 3 new ones (production fleet plus an
isolated instance to capture the rollout preview dialog in both a
safe and a blocked state), and fixed one stale pin-workflow sentence
in blueprint-model.mdx that the same audit surfaced.

* docs(fleet-federation): drop tier-availability framing

The feature isn't tier-gated, so calling out "every tier" reads as an
unnecessary advertisement. Removed the tier note and the Community/
Admiral prerequisite row, and reworded two role-visibility sentences
that had conflated tier with role.

* docs(fleet-actions): deep rewrite for the v2 action-card redesign

Rewrites the page against the shipped Fleet Action Card redesign: a
single cyan rail with per-card action-class chips instead of the old
per-card rose/purple/amber rails, plus the live blast-radius preview,
dry-run, and confirmed-target binding mechanics that replaced the old
static warning banners. Documents two previously-undocumented
endpoints (match-preview, prune/estimate), the preview-confirm-execute
model shared by all three cards, per-node locking, timeouts, and the
version-gated confirmed-target contract for mixed-version fleets. All
five screenshots recaptured against the live production UI.

* docs(fleet-actions): fix nested quote in screenshot alt text

* docs(fleet-sync): refresh against current app and document proxy gap

Retake all screenshots against the live fleet (control authoring view,
a genuine replica showing the read-only banner, and replicated
suppression rows) and document a previously-unwritten behavior: scan
policies, CVE suppressions, and misconfig acknowledgements are fetched
localOnly and are not proxied through the node switcher like most
other Security page tabs, so viewing a remote's Fleet Sync state
requires signing into that instance directly.

* docs(fleet-backups): refresh against current app and note new integration points

Replaces all 6 screenshots with fresh 1920x1080 production captures and
corrects several drifted details: the Cloud Snapshots panel's pagination
and per-item delete action, the exact Recovery Vault storage-mode label,
and the real per-file size-cap behavior (an oversized compose.yaml skips
the whole stack; an oversized .env is dropped but the stack still
captures). Documents two entry points that were missing from the page:
the pre-update snapshot checkbox in Health-Gated Updates and the snapshot
coverage nudge on the Storage Portability tab. Expands the single warning
banner description in the detail view to cover all four banner types,
notes the Snapshots tab and Recovery Vault are hub-only, and adds a
Where Fleet Backups fits table cross-linking the six adjacent features.

* docs(remote-updates): refresh against current app and add hardened-channel notes

Replace all screenshots with fresh 1920x1080 production captures and fix the
Node Updates trigger label (Check Updates -> Node Update). Add the Changelog
tab, correct the reconnect overlay's stale 'Update timed out'/Try Reloading
copy to the current Taking longer than expected/Reload to check text, and
document the Admiral Hardened Build channel's carve-outs (pinned-but-not-blocked,
entitlement-gated local update path). Disambiguate this page from the
unrelated top-level Update (Health-Gated Updates) nav tab.

* docs(node-compatibility): refresh against current app and expand capability list

Replace all screenshots with fresh production captures (node switcher,
capability lock card, connection test panel) and swap the lock-card example
from a now-hidden Host Console to Audit Log, which is directly reachable.
Bring the capability table from 26 to 35 entries to match the current
registry, add a Mute Rules row, and split out fine-grained fallback
capabilities (update-guard, service-scoped-update, cross-node-rbac,
stack-down-remove-volumes) with their non-lock-card fallback behavior.
Fix the compose-networking row, which incorrectly claimed to also gate the
node-level Networking overview. Note that Pilot Agent nodes never advertise
host-console regardless of version.

* docs(security): refresh against current app and document scan-node launcher

Replaces all screenshots with fresh production captures, documents the
Scan this node launcher and the third Scanner setup toggle (pre-deploy
scan advisory), tightens the Compose risks example list and the Policies
block-condition description to match the live app, and adds an On a
phone section.

* docs(vulnerability-scanning): refresh for exploit intel and risk-based policies

Documents exploit intelligence (KEV/EPSS evidence tags), the redesigned
risk-based scan policies (severity/known-exploited/fixable block
conditions replacing the single max-severity field), and the new
Findings badge state. Replaces all screenshots with fresh production
captures and adds a Secrets tab example.

* docs: refresh CVE suppressions page

Retook all screenshots against production (prior ones predated the
triage-status/OpenVEX UI and the edit capability). Documented the
suppression edit flow, which the page previously described as
delete-and-recreate only. Corrected the remote-node banner copy on the
Suppressions tab and added a screenshot of it. Cross-linked the
Misconfig acknowledgements panel that now sits on the same tab.

* docs(two-factor-authentication): refresh screenshots and document rate-limit layers

Replace all 11 non-count-variant screenshots with fresh production captures.
Document the throttled sign-in state precisely (kicker/hero/caption change,
not just the input), that a replayed TOTP counts toward the lockout counter,
and the separate per-network-address sign-in rate limit that sits in front
of the per-account MFA lockout.

* docs(api-tokens): refresh screenshots and document service-scoped deploy actions

Deploy Only now authorizes eight lifecycle POSTs (was six): the per-service
update and restore endpoints were missing from the scope table. Universal
restrictions gained a row for image channel management, which was already
rejecting API tokens in code but undocumented. Replaced all five screenshots
with fresh captures showing the current three-scope create flow and a
populated list with one token of each scope.

* docs(upgrade): refresh against current app and remove legacy-version framing

Fixes migration-list inaccuracies (registry credentials were never
plaintext, unlike node API tokens), rephrases the SSH/TLS and JSON-config
migration bullets to drop version-numbered legacy framing, adds a GHCR
mirror note matching the quickstart pattern, and cross-links the Hardened
Build entitlement-gated update path so digest-pinned installs aren't sent
down the manual docker pull steps.

* docs: refresh Backup & Restore against current backend

Broadens the encryption.key warning to cover everything CryptoService
actually encrypts (node tokens, Git source and Fleet Secrets, SSO/MFA,
Recovery Vault, fleet snapshot contents), not just registry credentials.
Adds the built-in backupData CLI as a no-host-tooling alternative to the
sqlite3 .backup command, cross-linked to Emergency command-line recovery.
Corrects node-token migration guidance: tokens are signed with a secret
stored in sencho.db and remain valid after a host move, so no
regeneration is required.

* docs: refresh Recovery guide against current Settings page and CLI

Documents the Settings · Recovery page's full System health / Environment /
Safe actions / Command-line hub instead of only its environment-preflight
section, adds the missing "Sencho compose location" preflight check, adds the
SSO/OIDC/LDAP lockout scenario now that disableSso.js exists, and corrects the
rollback description (exact UI label, backup-required and image-layer caveats).

* docs(emergency-cli): sync in-app page description, add CLI operational details

Aligns the "in-app Recovery page" summary with the freshly refreshed Recovery
guide (System health / Environment / Safe actions / Command-line recovery, SSO
providers, one-click command download). Adds constraints verified against the
CLI source: password/username minimums, the valid SSO provider identifiers with
an example, and a note that diagnostics.js always reports Docker as unreachable
(it runs without a live Docker connection, unlike the in-app page).

* docs(troubleshooting): refresh against current backend and nav

Corrects several claims that had drifted from the implementation:
network name validation (underscore is not a valid leading character),
the remote-update delayed-failure window (3 minutes, not 90 seconds),
and an unsubstantiated version-compatibility check on the Admiral 403
entry. Removes two entries describing legacy pre-v0.39 node behavior
that no longer applies to any currently shipped build. Replaces stale
"Profile > Settings > X" navigation with the current Settings hub
paths, and "wifi icon" with the current Test Connection label. Adds
Pilot Agent awareness to the remote-offline entry with a cross-link to
its dedicated troubleshooting section, and trims the docker-run
converter entry to point at the fuller, already-current version on the
Stack Management page instead of duplicating it with a broken anchor.

* docs(verifying-images): document the :dev GHCR integration tag

Verified the existing cosign/SBOM/VEX/tag-policy content against
docker-publish.yml and docker-preview.yml; all accurate. Added the
previously undocumented :dev/:dev-<sha> integration tag published on
every push to main via docker-dev.yml.

* docs(trivy-setup): sync install guide with node-scoped scanner and current Trivy upstream docs

Documents that Trivy installs independently per node, adds the TRIVY_BIN
override and non-PATH mounting option, fixes the deprecated apt-key install
flow and RHEL repo gpgkey placement, notes the Exploit intelligence toggle
for air-gapped hosts, and replaces the stale Scanner setup screenshot.

* docs(two-factor-admin): document lockout recovery and self-reset gotcha

Verified the reset flow, CLI recovery, and SSO toggle claims against
current source and replaced the stale admin-reset modal screenshot.
Added the failed-attempt lockout behavior (undocumented despite being
promised in the page description) and a warning about resetting your
own 2FA from the Users list, which signs you out immediately unlike
the self-service disable flow.

* fix(docs): make Settings and Self-hosting discoverable in the sidebar

The Reference and Operations groups used root pages that were only
reachable by clicking the ambiguous group label, and that click also
triggered an unwanted double action (expand plus navigate). List both
pages as explicit sidebar entries and disable global drilldown so group
headers only expand or collapse.

* docs(settings): sync reference page with current Settings Hub

Renamed License to Admiral Account throughout (Hardened Build channel
switch, image channel display, DURATION pill), corrected the password
policy, documented the new Appearance navigation and log-chip-color
controls plus font options, noted Mesh data plane is not on every
installation, fixed the label cap (50, not 100), and replaced every
screenshot with a fresh capture from the production node.

* fix(docs): apply the sidebar toggle-only fix to Start here and Product guide

These two groups had the same click-to-navigate-and-expand pattern as
Reference and Operations. Flattening them is safe here too: the
Product guide root page has no directory listing depending on it, and
the Start here root page's own hand-authored Next steps CardGroup
already covers the same links the auto-generated listing duplicated.

* docs(licensing): refresh licensing page for Hardened Build and sales-led Admiral pricing

Admiral pricing moved from self-serve checkout to a contact-sales model, and
a new Hardened Build image-channel switcher shipped in the Admiral Account
settings page; neither was reflected in the docs. Also documents the
lifetime-license edge case (no Manage subscription button, no Billing row)
and refreshes all four screenshots against the current UI.

* docs(security): sync reference page with current API-token and encryption scope

Verified every claim against the live implementation: the API-token universal
restrictions list was missing MFA management, Recovery Vault, and image
channel management; the encryption-at-rest field list was missing Recovery
Vault credentials; added a note on the password strength indicator's
recommended-vs-enforced distinction. Refreshed the SSO settings, API tokens,
and audit log screenshots against the production node.

* docs(settings): fix password policy and session-invalidation claims

Cross-checked against auth.ts while verifying the same claims on the
security reference page: the enforced minimum is 8 characters (the
12+/mixed-case/number text is a frontend strength hint, not a validated
rule), and a password change invalidates every other session for the
account rather than leaving them valid.

* docs(contact): sync contact channels with sales-led pricing and current support gating

Replace the retired contact@sencho.io with hello@sencho.io (the address actually
used in the website footer and the pricing page's Get in touch CTA), narrow the
licensing@sencho.io scope to existing-license activation/billing/refunds now that
new Admiral conversations route through hello@sencho.io, drop the stale LICENSE-file
and in-app upgrade-prompt claims (the AGPLv3 relicense removed both), correct the
Support settings path and add the published response-time targets, and remove the
unsubstantiated bug bounty mention.
This commit is contained in:
Anso
2026-07-21 09:13:12 -04:00
committed by GitHub
parent a4d9fa79a1
commit b1decbb32a
357 changed files with 1622 additions and 806 deletions
+7 -7
View File
@@ -76,6 +76,9 @@
"display": "simple"
}
},
"interaction": {
"drilldown": false
},
"navigation": {
"tabs": [
{
@@ -83,17 +86,16 @@
"groups": [
{
"group": "Start here",
"root": "getting-started/introduction",
"directory": "card",
"pages": [
"getting-started/introduction",
"getting-started/quickstart",
"getting-started/configuration"
]
},
{
"group": "Product guide",
"root": "features/overview",
"pages": [
"features/overview",
{
"group": "Core workflow",
"expanded": true,
@@ -184,9 +186,8 @@
},
{
"group": "Operations",
"root": "operations/self-hosting",
"directory": "accordion",
"pages": [
"operations/self-hosting",
"operations/upgrade",
"operations/backup",
"operations/recovery",
@@ -199,9 +200,8 @@
},
{
"group": "Reference",
"root": "reference/settings",
"directory": "accordion",
"pages": [
"reference/settings",
"features/licensing",
"reference/security",
"reference/contact"
+7 -4
View File
@@ -21,7 +21,7 @@ An API token is an opaque secret value (not a JWT), 56 characters long, that Sen
The three scopes form a ladder:
- **Read Only** authorises every `GET` request the underlying API exposes.
- **Deploy Only** adds the six stack-lifecycle POSTs: deploy, down, restart, stop, start, update.
- **Deploy Only** adds the eight stack-lifecycle POSTs: deploy, down, restart, stop, start, update, plus per-service update and restore.
- **Full Admin** adds every other read and write on the underlying API.
A separate layer sits on top: a set of universal restrictions that **no** API token can reach, regardless of scope or HTTP method. These cover identity, trust roots, billing, and interactive shell access; even a Read Only `GET` against `/api/users` or `/api/sso/config` is rejected. They require a human user session.
@@ -145,7 +145,7 @@ Authorises **every** `GET` against the API. Representative uses: list stacks, fe
### Deploy Only
Authorises every `GET`, plus exactly six `POST` patterns that operate on a stack by name:
Authorises every `GET`, plus eight `POST` patterns that operate on a stack (or one of its declared services) by name:
| Method | Path pattern | Action |
|--------|--------------|--------|
@@ -155,6 +155,8 @@ Authorises every `GET`, plus exactly six `POST` patterns that operate on a stack
| `POST` | `/api/stacks/:name/stop` | Stop without removing. |
| `POST` | `/api/stacks/:name/start` | Start a stopped stack. |
| `POST` | `/api/stacks/:name/update` | Pull newer images and redeploy. |
| `POST` | `/api/stacks/:name/services/:service/update` | Update or rebuild one declared Compose service without recreating its siblings. |
| `POST` | `/api/stacks/:name/services/:service/restore` | Roll a single service back to a prior recovery snapshot. |
Any other write (creating a stack, editing a stack file, calling a non-lifecycle action) returns `403 SCOPE_DENIED`.
@@ -190,6 +192,7 @@ Regardless of scope, every API token is rejected from the following routes with
| **Host-console session token** | Mint the short-lived ticket the browser console uses |
| **API token self-management** | Create, list, revoke API tokens |
| **Recovery Vault** | Configure or trigger Recovery Vault |
| **Image channel management** | Preflight or switch the instance's image channel, acknowledge a failed switch |
The rationale is the same in every case: a programmatic credential should not be able to grant itself more authority, change the trust roots that issued it, or open an interactive shell. Those actions require a live human user logged into a browser.
@@ -337,8 +340,8 @@ Read Only is sufficient for the notification stream.
The scope does not authorise the method or path you called. Either widen the scope by issuing a new token, or change the call. Common cases:
- Read Only attempting a `POST` of any kind.
- Deploy Only attempting a write that is not one of the six lifecycle actions (`deploy`, `down`, `restart`, `stop`, `start`, `update`).
- Any scope attempting a [universal restriction](#universal-restrictions) (user management, SSO, MFA, license, registries, console, node management, cloud backup, API-token self-management).
- Deploy Only attempting a write that is not one of the eight lifecycle actions (`deploy`, `down`, `restart`, `stop`, `start`, `update`, and the per-service `update`/`restore`).
- Any scope attempting a [universal restriction](#universal-restrictions) (user management, SSO, MFA, license, registries, console, node management, cloud backup, image channel management, API-token self-management).
</Accordion>
<Accordion title="`400 Maximum of 25 active API tokens per user`">
+14 -7
View File
@@ -45,23 +45,23 @@ Tiles are sorted by GitHub star count, descending. Click any tile to open the de
On small screens, the App Store adapts to a single-column layout. The masthead shows the store label, the total app count, the category count, and an action menu for navigation. Templates are listed alphabetically rather than by star count, and the category sidebar and featured hero are not shown on mobile.
<Frame>
<img src="/images/app-store/app-store-mobile.png" alt="App Store on a mobile device: status masthead showing APP STORE, 202 apps, and 12 categories, with an alphabetical single-column template list below" />
<img src="/images/app-store/app-store-mobile.png" alt="App Store on a mobile device: status masthead showing APP STORE, 201 apps, and 12 categories, with an alphabetical single-column template list below" />
</Frame>
## Deploying a template
The deployment sheet slides in from the right. The header carries a breadcrumb (`App store <template>`), the template name in the cockpit display face, and a meta line that combines the supported architectures, the GitHub star count, and the target node name when you are deploying to a remote node.
The deployment sheet slides in from the right. The header carries a breadcrumb (`App store <template>`), the template name as the sheet's title, and a meta line built from whichever of these the registry and deploy target supply: architecture chips, the GitHub star count, and the target node name when you are deploying to a remote node.
### About
The first section is a description block with the template logo, the full description, a **Read more** / **Read less** toggle, and a row of secondary metadata: architecture chips (for example `amd64`, `arm64`), GitHub stars, a **Source** link to the template's repository, and a **Docs** link to the project's official documentation when the registry provides one.
The first section is a description block with the template logo, the full description, a **Read more** / **Read less** toggle, and a row of secondary metadata: the GitHub star count, and, when the registry payload includes them, architecture chips (for example `amd64`, `arm64`), a **Source** link to the template's repository, and a **Docs** link to the project's official documentation. The bundled LinuxServer.io registry currently supplies only the star count here; a custom registry populates the rest when its template JSON includes `architectures`, `github_url`, and `docs_url`.
### Essentials tab
The **Essentials** tab is intentionally minimal: a single **Stack Name** field and a hint that the template's defaults will be used as-is. Click **Deploy {template}** to ship it.
<Frame>
<img src="/images/app-store/deploy-sheet-essentials.png" alt="Deployment sheet open on the Essentials tab, showing the App store breadcrumb, the template hero, an About panel with Read more, the Stack Name field pre-filled with the template slug, and the Deploy with defaults hint card" />
<img src="/images/app-store/deploy-sheet-essentials.png" alt="Deployment sheet open on the Essentials tab for qbittorrent, showing the App store breadcrumb, the About panel with Read more, the Stack Name field pre-filled with the template slug, and the Deploy with defaults hint card" />
</Frame>
The stack name pre-fills with the template slug (lowercased, hyphens, no spaces). You can change it; the same [naming rules](/features/stack-management#creating-a-stack) apply.
@@ -75,7 +75,7 @@ For anything beyond the defaults, switch to **Advanced**.
The **Advanced** tab exposes every knob the template declares. Every field is pre-populated with the template's recommended value, so customisation is additive rather than required.
<Frame>
<img src="/images/app-store/deploy-sheet-advanced.png" alt="Deployment sheet on the Advanced tab for radarr, showing Ports with a host port input, Volumes with three container mount points and editable host paths, and Environment Variables with PUID and PGID defaults" />
<img src="/images/app-store/deploy-sheet-advanced.png" alt="Deployment sheet on the Advanced tab for qbittorrent, showing three host port inputs, a single Volumes mount point, Environment Variables with PUID, PGID, and Timezone, a Custom variables row, and the Security scan checkbox" />
</Frame>
#### Ports
@@ -98,7 +98,11 @@ For each container mount point the template declares, type the **host path** whe
#### Environment variables
Each template declares the variables it expects. Sencho pre-fills sensible defaults where it can, and adds three baseline LinuxServer.io conventions automatically when the template does not already define them: **PUID** (`1000`), **PGID** (`1000`), and **TZ**, seeded from your browser's reported timezone.
Each template can declare the variables it expects, and Sencho pre-fills defaults where the registry supplies them. Every deploy also adds three baseline LinuxServer.io conventions automatically when the template does not already define them: **PUID** (`1000`), **PGID** (`1000`), and **TZ**, seeded from your browser's reported timezone.
<Note>
With the bundled LinuxServer.io registry, these three baseline variables are currently the only ones that appear in this section, regardless of the app: template-specific variables (a password, an API key, a subdomain) are not pulled through from that registry today. Check the app's own documentation (linked from its GitHub page) for the variables it needs and set them yourself using **Custom variables** below. A custom registry that supplies its own `env` array in its template JSON is not affected by this and populates the section normally.
</Note>
#### Custom variables
@@ -147,7 +151,7 @@ By default Sencho uses LinuxServer.io's hosted catalogue. To point at your own:
2. Type your catalogue URL into **Registry URL**. The URL must start with `http://` or `https://` and serve a JSON array of template objects in the [Portainer v2 template format](https://documentation.portainer.io/v2.0-be/api/templates/).
3. Click **Save & refresh**. Sencho persists the new URL and immediately invalidates the template cache so the App Store reloads from the new source.
A small chip under the input reads **using custom registry** when an override is set and **using default** when the field is empty. Click **Reset to default** to clear the override and fall back to LinuxServer.io.
A hint next to the **Save & refresh** button reads **using custom registry** when an override is set and **using default** when the field is empty. Click **Reset to default** to clear the override and fall back to LinuxServer.io.
The default registry display at the top of the page is informational only; you cannot remove the LinuxServer.io fallback, and Sencho uses it whenever the custom field is empty.
@@ -166,6 +170,9 @@ The default registry display at the top of the page is informational only; you c
<Accordion title="The Security checkbox is missing from the Advanced tab">
The **Scan images for vulnerabilities after deploy** checkbox renders only when the active node reports that Trivy is installed. Configure scanning per the [Vulnerability Scanning](/features/vulnerability-scanning) page, then reopen the deployment sheet. Even with the checkbox missing, the rest of the Advanced tab works exactly the same; you just lose the post-deploy scan trigger.
</Accordion>
<Accordion title="An app needs a variable that isn't in Environment Variables">
Some apps require a variable Sencho doesn't pre-fill for you, most commonly a password (code-server's `PASSWORD`, for example). With the bundled LinuxServer.io registry, the Environment Variables section only ever shows the PUID/PGID/TZ baseline, not the app's own declared variables. Check the app's GitHub page (linked from its LinuxServer.io listing) for the full variable list, then add what you need through **Custom variables** on the Advanced tab before deploying. If you skip a required variable like a password, the app typically starts with no authentication rather than failing to start, so add it up front rather than discovering the gap afterward.
</Accordion>
</AccordionGroup>
## Related
+20 -13
View File
@@ -1,9 +1,9 @@
---
title: Appearance
description: Personalize Sencho's look. Choose a visual style, tune readability and the security charts, pick a theme and accent, swap the interface and data fonts, and set the text size. Every choice is saved to the current browser.
description: Personalize Sencho's look. Choose a visual style, tune the security charts and readability, pick a theme and accent, swap the interface and data fonts, and set the text size. Every choice is saved to the current browser.
---
**Settings → Appearance** is where you make Sencho look the way you want. Choose between the Calm and Signature visual styles, dial in readability, set how the security charts are colored, pick a theme and accent, choose the interface and data fonts, and scale the text. A live preview reflects every change as you make it.
**Settings → Appearance** is where you make Sencho look the way you want. Choose between the Calm and Signature visual styles, set how the security charts are colored, dial in readability, pick a theme and accent, choose the interface and data fonts, and scale the text. A live preview reflects every change as you make it.
<Frame>
<img src="/images/settings/appearance-overview.png" alt="The Settings → Appearance page showing the Visual Style section with two preview cards: Calm (selected, with upright Critical heading and muted severity-color dots) and Signature (with italic serif Critical heading and saturated dots). Below are the Security Visualization chart-palette control with Muted selected, and the Readability section with its mode toggle, header-style control, and contrast slider." />
@@ -26,14 +26,7 @@ The **Visual style** is the master switch for how intense the interface feels:
- **Calm** (the default) keeps headings upright in the interface font, colors the security charts with a muted palette, and reduces material effects. It is the most readable setting.
- **Signature** uses italic Instrument Serif headings and the fully saturated chart palette for a more editorial look.
Picking a style sets the heading, chart, and effects controls below in one move. You are free to adjust any of them afterward to build your own combination. When **Readability mode** is on it overrides these until you turn it off.
## Readability
**Readability mode** is a single switch for the most legible result: upright headings, the muted chart palette, reduced effects, and a small contrast lift, all at once. While it is on, the heading and chart controls are locked to their calm values. Turn it off to set them by hand.
- **Header style** chooses **Clean** (the upright interface face) or **Signature** (italic Instrument Serif). Decorative moments such as the Sencho wordmark stay in the serif either way.
- **Contrast** is a master control that spreads the page tone, every border, and the text tiers together. Higher contrast darkens the page, brightens borders, and crisps up secondary text. Pairing high contrast with the OLED theme gives the sharpest reading on a true-black panel.
Picking a style sets the chart, header, and effects controls below in one move. You are free to adjust any of them afterward to build your own combination. When **Readability mode** is on it overrides these until you turn it off.
## Security visualization
@@ -43,17 +36,31 @@ The **Chart palette** sets the colors used on the Security page charts:
- **Heat** is a single warm ramp from a deep red through amber, with no competing hues.
- **Signature** is the saturated severity set.
## Readability
**Readability mode** is a single switch for the most legible result: upright headings, the muted chart palette, reduced effects, and a small contrast lift, all at once. While it is on, the heading and chart controls are locked to their calm values. Turn it off to set them by hand.
- **Header style** chooses **Clean** (the upright interface face) or **Signature** (italic Instrument Serif). Decorative moments such as the Sencho wordmark stay in the serif either way.
- **Contrast** is a master control that spreads the page tone, every border, and the text tiers together. Higher contrast darkens the page, brightens borders, and crisps up secondary text. Pairing high contrast with the OLED theme gives the sharpest reading on a true-black panel.
## Motion & effects
<Frame>
<img src="/images/settings/appearance-motion-effects.png" alt="The Appearance Motion & effects section with a Constrained graphics callout above Reduced motion (off) and Reduced effects (on, with its full helper text about flattening bevels and disabling glass blur), an Ambient glow slider at 0.16 with a Reset to default button, and the start of the Theme section's live preview card below it." />
</Frame>
- Appearance shows a **Constrained graphics** callout whenever Reduced motion is off, pointing you at it as the first thing to try if the active Sencho tab feels heavy on integrated graphics or similar devices.
- **Reduced motion** minimizes interface animations and transitions (dialogs, menus, expand and collapse). Toasts are unaffected. The **Calm** visual style turns this on by default; choosing **Signature** turns it off. You can still toggle it independently afterward without changing which visual-style card is selected. On constrained graphics, this is the first control to try when the active Sencho tab feels heavy on idle.
- When Reduced motion is off, Appearance shows a short notice about constrained graphics and points you at Reduced motion.
- **Reduced effects** flattens card bevels, the accent glow, and chart gradients, and uses a simpler rendering path: glass blur is off and chrome fills are solid. It also stops the decorative masthead rail animations even if Reduced motion is off. Fresh installs get this through the default **Calm** visual style. Treat it as an optional secondary experiment for material cost; it is not a substitute for Reduced motion on the reported idle-GPU symptom. The impact varies by device, browser, and graphics configuration.
- **Ambient glow** sets the intensity of the soft, accent-tinted glow behind the page. Slide it to zero for a completely flat background. (Reduced effects already dims it.)
- **Ambient glow** sets the intensity of the soft, accent-tinted glow behind the page. Slide it to zero for a completely flat background. (Reduced effects already dims it and disables the slider.)
- A **Reset to default** button returns the Visual style to Calm. It is disabled while Readability mode is on.
If the active Sencho tab feels heavy on constrained graphics, see [High GPU usage on constrained graphics](/operations/troubleshooting#high-gpu-usage-on-constrained-graphics).
## Theme
The Theme section opens with a live preview card that mirrors a real fleet-health card (sample value, title, subtitle, and icon swatches plus page/card/band/well surface tones and outline/primary buttons), updating instantly as you adjust the controls below it so you can see a change before committing to it.
The **Mode** control sets the overall surface palette:
- **Dim** (default) is a raised charcoal. Cards and panels sit clearly above the page without going fully black.
@@ -61,7 +68,7 @@ The **Mode** control sets the overall surface palette:
- **Light** is the bright theme, with the same directional, opaque borders inverted for a white surface.
- **Auto** follows your operating system, switching between Dim and Light as your OS does, and re-resolving live when the OS flips.
The **accent** is Sencho's one data color. It drives charts, sparklines, progress bars, focus rings, active states, the masthead and sidebar rails, and the ambient page glow, so changing it recolors the whole product cohesively. Eight well-spaced hues are available: **Cyan** (the default and the signature), **Blue**, **Violet**, **Magenta**, **Orange**, **Amber**, **Lime**, and **Steel** (a deliberate near-neutral). Status colors (green for healthy, amber for warning, rose for error) stay constant regardless of accent so state always reads the same.
The **accent** is Sencho's one data color. It drives charts, sparklines, progress bars, focus rings, active states, the masthead and sidebar rails, and the ambient page glow, so changing it recolors the whole product cohesively. Eight well-spaced hues are available: **Orange**, **Amber**, **Lime**, **Cyan** (the default and the signature), **Blue**, **Violet**, **Magenta**, and **Steel** (a deliberate near-neutral). Status colors (green for healthy, amber for warning, rose for error) stay constant regardless of accent so state always reads the same.
**Border brightness** lifts or softens every hairline on its own, so the separation between cards reads exactly how you like it. A **Reset fine-tune** button returns contrast, border brightness, and ambient glow to their defaults.
+10 -4
View File
@@ -28,15 +28,17 @@ Atomic deployments wrap:
A scheduled image-update task uses the same atomic wrapper as a manual update, so a recurring update still takes a backup and rolls back automatically when a container crashes. Scheduled lifecycle actions (start, stop, restart) change no stack configuration and run `docker compose` directly without a backup.
[Blueprint](/features/blueprint-model) deploys are a separate, fleet-wide desired-state path and do not use this atomic wrapper: a failed Blueprint apply is not backed up or rolled back automatically.
## Manual rollback
The stack editor's action bar has a **More actions** overflow menu (the three-dot icon next to **Update**). **Rollback** sits at the top, with the most recent backup's timestamp beneath the label. Selecting it restores the backed-up files and re-runs `docker compose up -d` non-atomically, so the rollback does not nest inside another atomic wrapper and overwrite the good backup with the just-failed state.
<Frame>
<img src="/images/atomic-deployments/rollback-menu.png" alt="Stack editor header for the plex stack, with the More actions overflow menu open. The menu shows three entries: Rollback at the top with the backup timestamp beneath the label, then Scan config, then Delete." />
<img src="/images/atomic-deployments/rollback-menu.png" alt="Stack editor header for the plex stack, with the More actions overflow menu open. Rollback sits at the top with the backup timestamp beneath the label, followed by Scan config, a Mute submenu for the stack's notifications, and Delete." />
</Frame>
The menu entry is hidden when no backup exists for the stack, for example on a freshly created stack that has never been deployed. It is also hidden for users who lack the `stack:deploy` permission; the backend enforces that check as the authoritative guard.
The menu entry is hidden when no backup exists for the stack, for example on a freshly created stack that has never been deployed. It is also hidden for users who lack the `stack:deploy` permission; the backend enforces that check as the authoritative guard. The **Mute** entry next to it controls notification suppression for the stack and is unrelated to atomic deployments; see [Alerts & Notifications](/features/alerts-notifications).
After a failed deploy or update, the stack page also surfaces a **Roll back** button in the recovery panel alongside Retry, Restart, and Refresh. This is the same rollback action, triggered in response to a failure rather than invoked on demand. See [Deploy Progress](/features/deploy-progress#recovery-actions) for the full recovery actions reference.
@@ -52,7 +54,11 @@ Before a restore overwrites anything, Sencho re-hashes each backed-up file and c
## Rollback readiness
The **Stack Dossier** includes a **Rollback readiness** panel: a pre-flight read on whether rolling back will actually fix the problem. It evaluates several signals: whether a previous compose file exists and how old it is, whether a previous `.env` was captured, whether image tags are pinned or moving (moving tags are not reverted), the age of the last successful deploy, and whether healthchecks are defined to verify recovery. It also notes that application data (database rows, uploaded files, anything in a named volume) is outside the scope of any revert.
The **Stack Dossier** includes a **Rollback readiness** panel: a pre-flight read on whether rolling back will actually fix the problem. It carries an overall verdict (**Ready**, **Partial**, or **Not ready**) and evaluates five signals: whether a previous compose file exists and how old it is, whether a previous `.env` was captured, whether the previous image tag is pinned or moving (moving tags are not reverted), the age of the last successful deploy, and whether healthchecks are defined to verify recovery. A sixth, always-shown note covers application data: named volumes and bind-mounted data (database rows, uploaded files, anything outside the compose and env files) are outside the scope of any revert.
<Frame>
<img src="/images/atomic-deployments/rollback-readiness.png" alt="Rollback readiness panel in the Stack Dossier for the plex stack, showing a Partial verdict badge and five signal rows: Previous compose file (ready), Previous env file (ready), Previous image tag (a moving tag warning naming the rollback target), Last successful deploy (ready), and Healthchecks (missing)." />
</Frame>
Check this panel in the dossier before rolling back a stack that has been running for a while. A rollback reverts only the compose and env files; if the problem is in a volume or in a database migration that already ran, rolling back the compose file alone will not help. See [Stack Dossier](/features/stack-dossier) for the full readout.
@@ -65,7 +71,7 @@ Check this panel in the dossier before rolling back a stack that has been runnin
The entry is also hidden for users who lack the `stack:deploy` permission. Ask an admin to grant `stack:deploy` through **Settings · Access** if the entry does not appear.
</Accordion>
<Accordion title="The deploy succeeded but a service crashed seconds later">
The health probe is a 3-second window after `docker compose up -d` returns. Crashes after that window are out of scope for atomic rollback, because Sencho cannot tell a late exit apart from a normal restart. The [health gate](/features/health-gated-updates) covers exactly this period: it observes the stack for a configurable window after the update, records a verdict on the stack timeline, and offers a manual rollback when containers do not stay healthy. For ongoing health beyond that, use **Auto-Heal Policies** to restart unhealthy containers automatically and **Alert Rules** to page you when a container exits unexpectedly.
The health probe is a 3-second window after `docker compose up -d` returns. Crashes after that window are out of scope for atomic rollback, because Sencho cannot tell a late exit apart from a normal restart. The [health gate](/features/health-gated-updates) covers exactly this period: it observes the stack for a configurable window after the update, records a verdict on the stack timeline, and offers a manual rollback when containers do not stay healthy. For ongoing health beyond that, use [Auto-Heal Policies](/features/auto-heal-policies) to restart unhealthy containers automatically and [per-stack alert rules](/features/alerts-notifications#per-stack-alert-rules) to page you when a container exits unexpectedly.
</Accordion>
<Accordion title="The deploy progress modal showed 'Rollback failed. Manual intervention may be required'">
This message means the auto-rollback attempted to restore the backup and re-deploy, but the restore step or the re-deploy itself errored out. The backup files are still at `<DATA_DIR>/backups/<nodeId>/<stack>/`. To recover:
+19 -13
View File
@@ -4,7 +4,7 @@ description: Track every mutating action on your Sencho instance with a searchab
---
<Note>
Community keeps a rolling 14-day recent-activity audit API window. The Audit navigation view, CSV and JSON export, anomaly detection, and configurable retention beyond the recent window require a Sencho **Admiral** license.
Community keeps a rolling 14-day recent-activity audit API window, but has no **Audit** tab in navigation to browse it. The Audit navigation view, CSV and JSON export, anomaly detection, and configurable retention beyond the recent window all require a Sencho **Admiral** license.
</Note>
<Note>
@@ -37,10 +37,11 @@ Expanding a row in the Table view reveals additional detail:
### Example actions tracked
- Stack lifecycle: deploy, stop, start, restart, update, rollback, delete
- Stack lifecycle: deploy, take down, start, stop, restart, update, rollback, delete
- Stack creation, file edits, env file changes, label changes
- Per-service lifecycle (start, stop, restart of an individual compose service)
- Container operations: start, stop, restart
- Label management: create, update, delete, and label-based bulk actions
- Node management: add, update, delete, cordon, uncordon
- Fleet operations: backup creation, restore, deletion, single-node and fleet-wide updates
- Fleet replica role changes (re-anchor, demote to control)
@@ -48,10 +49,11 @@ Expanding a row in the Table view reveals additional detail:
- Blueprint federation pin updates
- Recovery Vault: config update, connection test, provisioning, snapshot upload, snapshot deletion
- User management: create, update, delete, role assignment and removal
- Password changes and node token generation
- Password changes, two-factor authentication reset, and node token generation
- License activation and deactivation
- Webhook and notification agent configuration
- Notification route management and testing
- Notification suppression rule management
- SSO configuration changes and connection tests
- API token creation and revocation
- Registry credential management
@@ -64,9 +66,9 @@ Expanding a row in the Table view reveals additional detail:
## Viewing the audit log
The **Audit** view in navigation is an Admiral governance surface: it appears for users whose role grants the `system:audit` permission (by default **Admin** or **Auditor**). Community still has a rolling **14-day recent-activity** audit API window; Admiral adds the full Audit view, statistics, export, anomaly annotations, and configurable retention beyond that window.
The **Audit** view in navigation is an Admiral governance surface: the tab itself only appears on an Admiral license, and Community accounts have no navigation entry point to Audit at all, though the underlying recent-activity API stays reachable directly. On Admiral, the tab is further limited to users whose role grants the `system:audit` permission, which by default means **Admin** or **Auditor**.
Navigate to the **Audit** tab in the sidebar when it is available for your role and plan. On Admiral the feed in that view is scoped to the last 14 days by default unless you configure longer retention; export and anomaly detection come with Admiral.
Navigate to the **Audit** tab in the top navigation when it is available for your role and plan. The feed then shows your full retained history rather than any fixed lookback window; retention defaults to 90 days and is configurable up to 365.
The page has two views, toggled from the segmented control in the card header: **Stream** (default) and **Table**. The card subtitle reports the total number of entries that match the current filters.
@@ -75,7 +77,7 @@ The page has two views, toggled from the segmented control in the card header: *
Stream gives you an at-a-glance read on activity. A signal rail at the top summarizes the last 24 hours across four tiles, and the feed below groups entries by day with severity dots, relative times, and inline anomaly callouts.
<Frame>
<img src="/images/audit-log/audit-stream.png" alt="Audit Log Stream view with the four-tile signal rail (Events 111 +91% vs 7d avg, Actors 1, Failure rate 9% with sparkline, Peak hour 06:00) above a day-banded chronological feed of admin POST and DELETE entries." />
<img src="/images/audit-log/audit-stream.png" alt="Audit Log Stream view with the four-tile signal rail (Events 53 +218% vs 7d avg, Actors 2, Failure rate 0%, Peak hour 21:00) above a day-banded chronological feed of admin POST and DELETE entries, including a first seen anomaly flag on one entry." />
</Frame>
**Signal rail tiles:**
@@ -96,11 +98,11 @@ Rows colored in rose or amber indicate failures; the tinting makes spikes of err
Table keeps the full-featured detail grid for power users: exact timestamps, method badges, action summaries, and status codes in fixed columns. Clicking any row expands it to show the full request path, IP address, node ID, and entry ID.
<Frame>
<img src="/images/audit-log/audit-log-overview.png" alt="Audit Log Table view with a filter strip (search box, All Methods dropdown, From and To date pickers) above a six-column table of admin POST, PUT, and DELETE entries against /api/blueprints, /api/notifications, and /api/stacks." />
<img src="/images/audit-log/audit-log-overview.png" alt="Audit Log Table view with a filter strip (search box, All Methods dropdown, From and To date pickers) above a six-column table of admin POST and DELETE entries against /api/webhooks, /api/notifications, and /api/scheduled-tasks." />
</Frame>
<Frame>
<img src="/images/audit-log/audit-log-expanded.png" alt="Audit Log Table view with the second row expanded to reveal Request Path /api/stacks/plex/restart, IP Address ::ffff:192.0.2.10, Node ID 1, and Entry ID #820 in a four-cell detail strip." />
<img src="/images/audit-log/audit-log-expanded.png" alt="Audit Log Table view with the second row expanded to reveal Request Path /api/webhooks/4, IP Address ::ffff:203.0.113.10, Node ID 1, and Entry ID #2472 in a four-cell detail strip." />
</Frame>
Both views share the **Refresh** button and the **Export** dropdown in the card header, and both paginate at 50 entries per page with chevron controls at the bottom of the feed or table.
@@ -145,6 +147,10 @@ Exports are capped at 10,000 entries per download. To export a larger window, na
## Auditor role
<Note>
Assigning the **Auditor** role requires a Sencho **Admiral** license; Community can assign **Admin** or **Viewer** only.
</Note>
The **Auditor** role provides read-only access to the audit log without granting any administrative privileges. Auditors can:
- View the full audit log
@@ -166,12 +172,12 @@ On Admiral, audit log entries are automatically pruned based on your configured
To change the retention period:
1. Open **Settings · Operations · Developer Diagnostics**
2. In the **Data retention** card, set the **Audit log** value (1 to 365 days)
1. Open **Settings · Operations · Data Retention**
2. Set the **Audit log** value (1 to 365 days)
3. Click **Save settings**
<Frame>
<img src="/images/audit-log/data-retention.png" alt="Data retention card with three rows: Container metrics 24 hrs, Notification log 30 days, and Audit log 90 days." />
<img src="/images/audit-log/data-retention.png" alt="Data Retention settings page with retention rows for Container metrics (24 hrs), Notification log (30 days), Scan history per digest (50 scans), Remove scans for deleted images and stacks (on), and Audit log (90 days)." />
</Frame>
Cleanup runs automatically as part of Sencho's periodic maintenance cycle.
@@ -183,8 +189,8 @@ Sensitive database values (such as remote node API tokens) are encrypted at rest
## Troubleshooting
<AccordionGroup>
<Accordion title="The Audit tab is missing from the sidebar">
The Audit tab is an Admiral governance view shown to users whose role grants the `system:audit` permission. By default that means **Admin** or **Auditor**. If you are signed in as a Deployer or Viewer, ask an admin to assign you the Auditor role from **Settings · Users**. Community still has a rolling 14-day recent-activity audit API window; export, anomaly detection, and retention beyond the recent window come with Admiral.
<Accordion title="The Audit tab is missing from the navigation">
The Audit tab is an Admiral governance view: it only appears on an Admiral license, and on Community it is not shown at all, regardless of role. On Admiral, the tab is further limited to users whose role grants the `system:audit` permission, by default **Admin** or **Auditor**. If you are on Admiral but signed in as a Deployer or Viewer, ask an admin to assign you the Auditor role from **Settings · Users**. The recent-activity API stays reachable on Community even without a navigation entry point to it.
</Accordion>
<Accordion title="Stream view shows everything but I want to filter to a specific user, action, or date">
Filters live in **Table view only**. Toggle the segmented control in the card header from **Stream** to **Table** and the search box, method dropdown, and From / To date pickers will appear above the grid. Switching back to Stream clears the filter strip but does not remember the last filter.
+8 -7
View File
@@ -20,7 +20,7 @@ Policies live next to your stack-level alert rules in the stack's **Monitor** sh
## Prerequisites
- For **healthcheck-based** healing, containers must declare a `HEALTHCHECK` in the Dockerfile or a `healthcheck` block in `docker-compose.yml` so they report a Docker health status. **Crash-based** healing needs no healthcheck: any container that exits with a non-zero code qualifies.
- You must be signed in as an admin.
- Viewing existing policies and their history is open to every signed-in role. Creating, toggling, or deleting a policy requires the admin role.
<Note>
Crash healing acts on crashes Sencho observes while it is running. A container that crashed before Sencho started is left for you to inspect rather than restarted automatically.
@@ -34,7 +34,7 @@ Policies live next to your stack-level alert rules in the stack's **Monitor** sh
4. Click **Add Policy**. The new policy appears in **Active policies** above the form, with its enable toggle already on.
<Frame>
<img src="/images/auto-heal-policies/sheet.png" alt="The Stack PLEX MONITOR sheet on the Auto-heal tab, showing one All-services policy in the ACTIVE POLICIES section with its ON toggle, and the ADD NEW POLICY form below with Service, Unhealthy for, Cooldown, Max restarts / hr, and Auto-disable after fields, plus the Add Policy button" />
<img src="/images/auto-heal-policies/sheet.png" alt="The Stack plex Monitor sheet on the Auto-heal tab, showing one policy scoped to the plex service in the Active policies section with its ON toggle, and the Add new policy form below with Service, Unhealthy for, Cooldown, Max restarts / hr, and Auto-disable after fields, plus the Add Policy button" />
</Frame>
You can add as many policies to a stack as you need. Each policy is evaluated independently.
@@ -53,6 +53,7 @@ You can add as many policies to a stack as you need. Each policy is evaluated in
- **All services** applies the policy to every container in the stack that reports `unhealthy`. Containers are matched by the `com.docker.compose.service` label.
- A **named service** scopes the policy to the containers for that Compose service only. Multiple policies can coexist on the same stack, so you can keep an aggressive policy on one service while running a more permissive default for the rest.
- If an **All services** policy and a named-service policy both cover the same container, only one restart fires for that container per evaluation pass. Give the two policies different thresholds deliberately if you want predictable behavior; otherwise whichever policy is evaluated first claims the restart.
## Safety rails
@@ -68,10 +69,10 @@ Each policy has four safety rails built in. They run before any restart.
## Managing policies
Each row in **Active policies** shows the service it targets, a compact summary of the thresholds, and three controls on the right: an **ON/OFF** toggle, a chevron that expands the history panel, and a trash icon that deletes the policy.
Each row in **Active policies** shows the service it targets and a compact summary of the thresholds. Every signed-in role sees the row and can expand its history; the **ON/OFF** toggle and the delete icon are visible only to admins.
<Frame>
<img src="/images/auto-heal-policies/policy-row.png" alt="Single Auto-heal policy row showing the threshold summary, ON toggle, history chevron, and delete button" />
<img src="/images/auto-heal-policies/policy-row.png" alt="Single Auto-heal policy row showing the plex threshold summary, ON toggle, history chevron, and delete button" />
</Frame>
- **Toggle a policy off** to pause evaluation without losing its configuration. This is useful when you need to take a service down for maintenance without auto-heal interfering.
@@ -83,7 +84,7 @@ Each row in **Active policies** shows the service it targets, a compact summary
Expand the chevron on any policy to reveal its history. Each entry shows the timestamp, the container that was acted on, a colored action label, and a one-line reason.
<Frame>
<img src="/images/auto-heal-policies/recent-activity.png" alt="Active policy row with the Recent activity panel expanded, showing No history yet" />
<img src="/images/auto-heal-policies/recent-activity.png" alt="The plex policy row with the Recent activity panel expanded, showing No history yet" />
</Frame>
The action label tells you what Auto-Heal did or, if it chose not to act, why it skipped.
@@ -137,7 +138,7 @@ The dashboard's **Configuration status** card surfaces an **Auto-heal policies**
Two safety rails are designed to stop this: the **Max restarts / hr** cap and the **Auto-disable after (failures)** threshold. If a container is unhealthy immediately after each restart, lower the cap and the failure threshold while you investigate the root cause in the container's logs. You can also raise **Unhealthy for (minutes)** so transient blips do not count against the policy.
</Accordion>
<Accordion title="I cannot see the Auto-heal tab on a stack">
Configuring Auto-Heal Policies requires an admin sign-in. If the **Auto-heal** tab is not present on the **Monitor** sheet, confirm you are signed in as an admin.
<Accordion title="I don't see the Add new policy form, or the ON/OFF toggle and delete icon">
The **Auto-heal** tab itself is visible to every signed-in role, including viewers, so you can always check what is configured and browse history. Creating a policy, toggling it on or off, and deleting it are admin-only actions; a non-admin role sees the policy list and its history but not those controls. Confirm you are signed in as an admin under **Settings → Users** if you expect to make changes.
</Accordion>
</AccordionGroup>
+33 -1
View File
@@ -75,6 +75,8 @@ Once the cause is resolved, the next check (on the interval, or via **Recheck**)
On multi-service stacks, the Updates view can also apply a single service when that service has a confirmed image update. Scheduled auto-update, webhook pull, and bulk update always refresh the full stack.
5. Use **Recheck** in the hero to force an immediate registry poll across every reachable node. A 2-minute per-node cooldown applies, and the toast tells you how many nodes were triggered, rate-limited, or failed.
**Apply now** runs the same update path as every other update trigger on a stack: Sencho takes an atomic backup of the compose and env files first, pulls the updated registry images (or rebuilds from source for services that declare `build:`, then pulls the rest), and recreates the containers. See [Controlling a running stack](/features/stack-management#controlling-a-running-stack) for exactly how a build-aware update differs from a plain pull. Once the containers are up, the [post-update health gate](/features/health-gated-updates#the-post-update-health-gate) observes them for a configurable window and records a passed, failed, or unknown verdict on the stack's timeline, the same as a manual update from the editor toolbar or sidebar.
## Risk badges
| Badge | Color | When it appears |
@@ -117,6 +119,8 @@ Auto-update is a first-class action in the Schedules view. To create a recurring
The task lives alongside restart, prune, snapshot, and scan tasks in the same timeline and table. Run history, notifications, and the Run Now button behave the same as for every other scheduled action. See [Scheduled Operations](/features/scheduled-operations) for details.
A scheduled run applies an update the same way **Apply now** does: an atomic backup first, then the pull or build-aware rebuild, then the [post-update health gate](/features/health-gated-updates) observing the result. A schedule never bypasses a [deploy enforcement](/features/deploy-enforcement) policy: a run that a policy would block fails with the policy violation recorded in the run history, rather than applying partway.
## Multi-node support
The readiness board shows pending updates from every node in your fleet in a single view, regardless of which node is selected in the sidebar. You do not need to switch nodes to inspect what is pending elsewhere.
@@ -127,7 +131,7 @@ Each node group renders its own card grid under a header showing the node name,
For each stack with a pending image update, Sencho computes a preview by:
1. Parsing the compose file to enumerate every pullable image reference.
1. Parsing the compose file to enumerate every service that pulls a registry image (a service declaring only `build:`, with no `image:` key, has no registry reference to check and is excluded from this preview).
2. Calling the registry with your configured credentials to fetch the current tag list and remote digest.
3. Picking the highest semver tag greater than the current tag (keeping the same prefix and suffix). If the highest available tag matches the current one but the remote digest has changed, the card surfaces as a `Rebuild available` update. Sencho checks both a newer semver tag and a digest change behind the same tag; see [Tags vs digests](/features/vulnerability-scanning#tags-vs-digests).
4. Scoring the overall stack by the most severe image bump. Any major bump marks the stack as blocked.
@@ -135,6 +139,8 @@ For each stack with a pending image update, Sencho computes a preview by:
The preview is recomputed each time the readiness board loads, so it reflects the live state of your registries and local images.
A stack that mixes registry images and `build:` services still gets a card, scored from its registry images. Triggering **Update** on that stack, from this board or a schedule, always rebuilds the `build:` services from source in the same run, whether or not the registry portion has a pending update. See [Controlling a running stack](/features/stack-management#controlling-a-running-stack) for what a build-aware update runs.
## Troubleshooting
<AccordionGroup>
@@ -162,4 +168,30 @@ The preview is recomputed each time the readiness board loads, so it reflects th
<Accordion title='Disk usage barely changed after an update'>
Automatic cleanup reclaims dangling (untagged) image layers only. A long-running host accumulates other reclaimable data (stopped containers, unused volumes and networks, build cache) that this toggle does not touch. Run a full prune from the [Resources](/features/resources) view, or schedule a prune task, to reclaim the rest.
</Accordion>
<Accordion title='A scheduled auto-update did not apply and the run history shows a policy error'>
A [deploy enforcement](/features/deploy-enforcement) policy blocked the update, the same way it would block a manual one. The run history and the dispatched alert name the policy and the matching image findings. Review the findings and either clear them or adjust the policy, then let the next scheduled run or a manual **Apply now** retry.
</Accordion>
</AccordionGroup>
## Related
<CardGroup cols={2}>
<Card title="Health-gated updates" icon="heart-pulse" href="/features/health-gated-updates">
The readiness check before an update and the post-update health gate that every apply path, including auto-update, runs through.
</Card>
<Card title="Atomic deployments" icon="rotate" href="/features/atomic-deployments">
How Sencho backs up compose and env files before each update and rolls back automatically on failure.
</Card>
<Card title="Deploy enforcement" icon="shield-check" href="/features/deploy-enforcement">
Set risk policies that block or require review before a manual or scheduled update can apply.
</Card>
<Card title="Stack management" icon="layer-group" href="/features/stack-management#controlling-a-running-stack">
The full set of lifecycle actions on a stack, including how a build-aware update differs from a plain pull.
</Card>
<Card title="Scheduled operations" icon="calendar-clock" href="/features/scheduled-operations">
How the Auto-update Stack and Auto-update All Stacks on Node tasks fit into the shared scheduling system.
</Card>
<Card title="Vulnerability scanning" icon="shield-halved" href="/features/vulnerability-scanning#tags-vs-digests">
Why a same-tag digest change surfaces as a rebuild instead of a version diff.
</Card>
</CardGroup>
+10 -11
View File
@@ -8,7 +8,7 @@ A **Blueprint** bundles a `docker-compose.yml` with a node selector and a drift
Blueprints live under **Fleet · Deployments**.
<Note>
Blueprints are available on every tier as Sencho's Compose-first orchestration model. Creating, editing, withdrawing, and applying blueprints requires an admin role; operators and viewers can read the catalog and the detail sheet. Pinning a blueprint to a specific node lives under **Fleet · Federation** on every tier (admin role required).
Blueprints are Sencho's Compose-first orchestration model. Creating, editing, withdrawing, and applying blueprints requires an admin role; operators and viewers can read the catalog and the detail sheet. Pinning a blueprint to a specific node lives under **Fleet · Federation** (admin role required).
</Note>
<Frame caption="Fleet · Deployments catalog with blueprint tiles, the All / Drifted / Observe / Suggest / Enforce filter chips, and the New Blueprint action in the top-right.">
@@ -47,7 +47,6 @@ Drift detection runs on every tick for every Active deployment regardless of pol
| Requirement | Detail |
|---|---|
| License tier | **Community** to read, create, edit, withdraw, apply, and pin blueprints. |
| User role | **Admin** to create, edit, withdraw, accept, and apply. Operators and viewers can read the catalog and the detail sheet. Pinning requires admin. |
| Nodes | At least one node that the selector resolves to. Remote nodes need a healthy proxy connection; see [Multi-node management](/features/multi-node) and [Pilot Agent](/features/pilot-agent) for enrollment. |
| Compose YAML | Valid `docker-compose.yml`, 96 KiB or fewer. |
@@ -88,8 +87,8 @@ Before a local Blueprint deploy starts, Sencho applies the same pre-deploy [vuln
**New Blueprint** opens a single editor dialog with everything inline: name and description fields, a Monaco YAML editor with the classification banner sitting above it, the selector picker (Labels or Specific nodes), the drift policy cards, and the Reconciler enabled toggle.
<Frame caption="The Blueprint editor. Compose YAML on the left with the classification banner pinned above it; selector mode tabs, drift policy cards, and the Reconciler enabled toggle below.">
<img src="/images/blueprint-model/editor-dialog.png" alt="Blueprint editor dialog with classification banner, selector, and drift policy" />
<Frame caption="The Blueprint editor. Name and description fields, the classification banner pinned above the compose editor, and the Specific nodes selector below. Drift policy cards and the Reconciler enabled toggle sit further down in the same scrollable dialog.">
<img src="/images/blueprint-model/editor-dialog.png" alt="Blueprint editor dialog with name, description, classification banner, compose editor, and node selector" />
</Frame>
### Classification banner
@@ -103,7 +102,7 @@ The banner above the YAML editor shows what Sencho found when it parsed your com
Click the signals toggle on the right of the banner (`1 SIGNAL`, `2 SIGNALS`, etc.) to expand the list of reasons Sencho classified the way it did, line by line.
<Frame caption="The classification banner expanded. The signals list shows the exact rule that fired for each detected mount, volume, or external reference.">
<img src="/images/blueprint-model/classification-banner.png" alt="Classification banner in the Stateless · portable variant with one signal expanded" />
<img src="/images/blueprint-model/classification-banner.png" alt="Classification banner in the Stateful · pins to data variant with two signals expanded" />
</Frame>
### Selector
@@ -130,7 +129,7 @@ The three policy cards in the editor map exactly to the three modes the reconcil
Even **Observe** keeps Sencho honest about what it found: the deployment row shows "drifted 3h ago: service caddy exited code 1". Silence would forfeit Sencho's authority over your fleet.
For **stateful** blueprints under Enforce, Sencho declines auto-fixes that would destroy named volumes (for example, when you rename a volume in the compose). The drift downgrades to Suggest semantics for that event with the reason `auto-fix declined: would destroy volume data`.
Enforce's auto-fix only ever redeploys the blueprint's *current* revision to correct container-level drift (a stopped or crashed container, for example). A compose edit never reaches that auto-fix path on a stateful or state-unknown blueprint: any revision change re-enters **Awaiting confirmation** on every targeted node first, regardless of drift mode, so a human always signs off before a new revision touches existing data. See [Lifecycle: status transitions](#lifecycle-status-transitions).
## The detail sheet
@@ -195,7 +194,7 @@ Raw `POST /api/blueprints/:id/apply` clients must send `{ planFingerprint, actio
### Edit
Click **Edit** on the detail sheet. Editing the compose bumps the revision. Stateless blueprints redeploy on the next reconciliation tick. Stateful and state-unknown blueprints re-enter **Awaiting confirmation** on every targeted node so the operator can decide whether the new revision is safe for each node's local data. Stateful blueprints also follow the volume-destroying-drift rule under Enforce: a compose change that would destroy named volumes downgrades to Suggest for that event.
Click **Edit** on the detail sheet. Editing the compose bumps the revision. Stateless blueprints redeploy on the next reconciliation tick. Stateful and state-unknown blueprints re-enter **Awaiting confirmation** on every targeted node so the operator can decide whether the new revision is safe for each node's local data, whether or not the change actually touches a volume.
### Confirm a stateful first deploy
@@ -227,7 +226,7 @@ In the deployment table, click **Withdraw** on the node's row. For stateless blu
### Delete the blueprint
Stateless blueprints withdraw all deployments and then delete in a single click. Stateful blueprints with active or pending deployments refuse to delete to avoid silent orphans. Withdraw each deployment explicitly first, then delete.
Click **Delete** on the detail sheet, then type the blueprint's name to confirm. Stateless deployments, and any deployment that never made it past **Awaiting confirmation**, are withdrawn for you as part of the delete. A stateful or state-unknown blueprint with a deployment that is live on a node refuses to delete; withdraw that deployment from the deployment table first, so you choose the snapshot-or-destroy path for its data, then delete the blueprint.
## Federation: pin a blueprint to a single node
@@ -254,7 +253,7 @@ Both events route through the standard alert pipeline. Configure delivery channe
## Security and trust boundaries
**Who can do what.** The license tier and the user role together determine the available actions. Reading the catalog, the detail sheet, and the deployment status is available on every tier. Creating, editing, withdrawing, accepting a stateful deploy, and applying on demand require the admin role. Pinning a blueprint requires the admin role.
**Who can do what.** The user role determines the available actions. Reading the catalog, the detail sheet, and the deployment status is open to every role. Creating, editing, withdrawing, accepting a stateful deploy, and applying on demand require the admin role. Pinning a blueprint requires the admin role.
**The marker file is the trust root.** The reconciler will only deploy into, modify, or withdraw a directory that carries a `.blueprint.json` marker whose blueprint ID matches. A pre-existing directory with no marker, or a marker referencing a different blueprint, surfaces as **Name conflict** and is never modified.
@@ -302,7 +301,7 @@ A future Volume Migration feature will automate this with app-aware backup tooli
**Single-node managed Postgres.** A stateful `pg-fleet` blueprint with a `nodes` selector pointing at one database node and drift mode **Suggest**. The first deploy enters **Awaiting confirmation** so the operator chooses **Deploy fresh**. Subsequent compose changes (image bump, config change) re-enter **Awaiting confirmation** on the same node so the operator can decide whether the new revision is safe for the existing volume. Drift on the running container fires a `blueprint_drift_detected` notification but never auto-redeploys.
**Pin a blueprint to a specific node despite the selector.** Open **Fleet · Federation**, find the blueprint in the pin policy table, and pick the target node from the **Pinned to** dropdown. The pin overrides the selector for that blueprint, deploys only to the pinned node, and also overrides cordon on that node. Useful for relocating a stateful service to a specific host without rewriting the selector. Clear the pin to restore selector-driven placement.
**Pin a blueprint to a specific node despite the selector.** Open **Fleet · Federation**, find the blueprint in the pin policy table, and pick the target node from the **Pinned to** dropdown. The pin saves immediately but also clears approval, so nothing moves until you open the blueprint here in Deployments, click **Apply now**, and **Confirm Apply**. Once confirmed, the pin overrides the selector for that blueprint, deploys only to the pinned node, and also overrides cordon on that node. Useful for relocating a stateful service to a specific host without rewriting the selector. Clear the pin (and confirm again) to restore selector-driven placement.
**Observe-only audit blueprint.** A stateless monitoring stack (Vector, Promtail, a Prometheus exporter) with drift mode **Observe**. Drift is recorded silently in the deployment table; no notification fires and no auto-fix runs. Useful when you want Sencho to track placement and detect divergence on a low-signal stack without paging anyone.
@@ -328,7 +327,7 @@ A future Volume Migration feature will automate this with app-aware backup tooli
The deployment row moves to **Failed** and records the Docker or registry error. Resolve the daemon, socket, registry credentials, rate limit, disk, or volume-permission issue on the affected node, then click **Apply now**. Watch that node's stack activity and security scan status after retry.
</Accordion>
<Accordion title="Drift is reported but never gets corrected">
Confirm the drift policy is **Enforce** and that **Reconciler enabled** is on. Open the detail sheet to check the row's status and the most recent drift summary. If the drift was caused by a compose change that would destroy named volumes, Enforce intentionally downgrades to Suggest semantics for that event. Either change the compose to one that preserves the volumes, or withdraw the deployment with explicit operator confirmation and let the new revision deploy fresh.
Confirm the drift policy is **Enforce** and that **Reconciler enabled** is on. Open the detail sheet to check the row's status and the most recent drift summary. If the row is sitting in **Awaiting confirmation** instead of **Drifted**, that is expected: a compose edit on a stateful or state-unknown blueprint never auto-corrects under any drift mode, including Enforce. It always waits for an explicit **Confirm deploy** so a human signs off before the new revision touches that node's data. Enforce's auto-fix only applies to container-level drift (a stopped or crashed container) on a deployment that is already **Active**.
</Accordion>
<Accordion title="'Apply now' is greyed out">
**Apply now** requires the blueprint to be enabled. Open the detail sheet, click **Enable** to flip the reconciler back on, then click **Apply now**. The button is also disabled while the detail sheet is in edit mode; save or cancel the edit first.
+43 -19
View File
@@ -5,7 +5,7 @@ description: Run a preflight check on a stack before you deploy. Compose Doctor
The **Doctor** tab in the right-hand **Anatomy** panel answers one question before you apply a change: *what will Docker actually run, and is it safe on this node?* Compose Doctor renders the effective Compose model (the fully resolved result after interpolation, includes, profiles, `.env`, and `env_file` are applied) and then runs a set of deterministic checks against it and the live Docker state on the node it would deploy to.
The check is advisory. It never blocks a deploy and never changes a stack. It runs on demand: press **run preflight** and Sencho renders the model, runs all 31 checks, and stores the result so the tab still shows it the next time you open the stack.
The check is advisory: on its own it never blocks a deploy or changes a stack. (One rule is the exception: see [Self-management](#self-management) below for the one case where Sencho actively blocks an action, independent of this report.) It runs on demand: press **run preflight** and Sencho renders the model, runs all 32 checks, and stores the result so the tab still shows it the next time you open the stack.
## Where to find it
@@ -24,7 +24,7 @@ Every preflight run follows three steps:
1. **Render** the effective model. Sencho calls `docker compose config` on the stack, which resolves all variable interpolation, `include` directives, profile overrides, and `env_file` references into a single, normalized model.
2. **Snapshot** live Docker state. Sencho reads which host ports are in use, which containers are running, and which named networks and volumes exist on the target node.
3. **Run 31 deterministic rules** against the combination. Each rule is pure and produces zero or more findings with a severity, a message, and a suggested fix.
3. **Run 32 deterministic rules** against the combination. Each rule is pure and produces zero or more findings with a severity, a message, and a suggested fix.
Sencho stores exactly one run per stack per node, so a new run immediately overwrites the previous one; there is no history.
@@ -54,14 +54,15 @@ The summary card at the top of the Doctor tab reflects the overall outcome of th
| **No run yet** | Preflight has never been triggered for this stack on this node | Placeholder with "no preflight yet" and a Stethoscope icon |
| **All clear** | Model rendered; no findings | Green card with "No issues found in the effective model." |
| **Cannot render** | `docker compose config` failed | Red card with the (redacted) render error message |
| **Blocker** | Highest finding is a blocker | Red card with a count of findings by severity |
| **High risk** | Highest finding is high risk | Amber card with a count of findings by severity |
| **Warning** | Highest finding is a warning | Blue card with a count of findings by severity |
| **Info** | All findings are informational | Muted card with a count of findings by severity |
| **Blocker** | Highest active finding is a blocker | Red card with a count of active findings by severity |
| **High risk** | Highest active finding is high risk | Amber card with a count of active findings by severity |
| **Warning** | Highest active finding is a warning | Blue card with a count of active findings by severity |
| **Info** | All active findings are informational | Muted card with a count of active findings by severity |
| **Acknowledged** | Every finding from the last run has been acknowledged | Muted card noting all findings are acknowledged |
The summary card also shows when the run happened and who triggered it: "ran 5 minutes ago by admin".
The summary card also shows when the run happened and who triggered it: "ran 5 minutes ago by admin". If any findings are acknowledged, the line adds a count: "3 active (1 blocker · 2 warning) · 2 acknowledged".
A small colored dot appears on the **Doctor** tab label when the last run found blockers (red) or high-risk issues (amber). Opening the tab removes neither the dot nor the findings; run preflight again after fixing the issues to clear them.
A small colored dot appears on the **Doctor** tab label when the last run's active findings include a blocker (red) or a high-risk finding (amber). Click the **X** on the summary card to dismiss it; this also clears the tab dot. The dismissal is remembered per stack and node, and clears itself automatically the moment the finding set changes (a rule clearing, a new rule firing, or a severity change), so a stale dismissal can never hide a genuinely new problem. It never clears just by opening the tab or re-running preflight to the same result, and it does not sync across browsers or teammates: it is stored in your browser only.
<Frame>
<img
@@ -72,7 +73,7 @@ A small colored dot appears on the **Doctor** tab label when the last run found
## What it checks
All 31 rules are listed below, organized by topic.
All 32 rules are listed below, organized by topic.
### Model rendering
@@ -154,24 +155,38 @@ These rules activate when the stack publishes at least one host port. They use t
| Published port not in documented access URLs | Warning | A host port is not referenced by any access URL recorded in the Stack Dossier; the documentation may be stale. |
| Has reverse-proxy labels but no documented URL | Warning | A service carries Traefik, Caddy, or similar reverse-proxy labels but has no documented access URL or reverse-proxy intent set. |
### Self-protection
| Rule | Severity | What it detects |
|------|----------|----------------|
| This stack is the running Sencho instance | Warning | Sencho discovered its own compose project among the managed stacks. See [Self-management](#self-management) below. |
## Exposure intent checks
Five of the 30 rules cross-reference the stack's exposure intent and the access URLs documented in the Stack Dossier. These rules only fire when the stack publishes at least one host port.
Five of the 32 rules cross-reference the stack's exposure intent and the access URLs documented in the Stack Dossier. These rules only fire when the stack publishes at least one host port.
To resolve exposure-related findings:
- **Set an exposure intent** in the [Compose Networking](/features/compose-networking) tab. Options are: internal (no host access), same-node (loopback only), LAN, reverse proxy (traffic arrives via a proxy), and public.
- **Set an exposure intent** in the [Compose Networking](/features/compose-networking) tab. Options are: internal (no host access), same-node (loopback only), LAN, reverse proxy (traffic arrives via a proxy), temporary (a short-lived exposure, tracked but not otherwise enforced), and public.
- **Document access URLs** in the [Stack Dossier](/features/stack-dossier). When a dossier URL references a host port, the port-vs-dossier rule uses that to confirm the port is intentional and expected.
Once an intent and access URLs are set, Sencho can detect when the configuration contradicts the documented intent, making future runs more precise.
## Self-management
Sencho recognizes when the stack it is checking is its own running compose project, and adds a warning finding: **This stack is the running Sencho instance**.
This is the one place Compose Doctor's findings connect to an enforcement that is not advisory. Independent of this report, and independent of whether the finding is acknowledged, Sencho refuses generic **deploy, update, stop, down, and delete** actions on its own compose project, because any of them could recreate or remove the dashboard you are using to manage it. Attempting one of those actions returns an error explaining why; Compose Doctor's finding is the heads-up that surfaces the same fact before you try.
To update Sencho itself, use **Fleet -> Node Update** instead of the stack's own update action. To manage the stack normally (for example, to deliberately restructure it), move its compose project outside the node's configured Compose directory first.
## Networking-relevant findings on the Networking page
The networking-relevant rules on this page (host mode, exposure intent, port conflicts, and the exposure-intent checks above) also surface on the node-wide [Networking](/features/networking) page's Findings tab, using the result of your last run here. A finding both engines detect is merged into one card; a finding only these Doctor rules catch appears there labeled with the run's timestamp. Acknowledging a finding here also excludes it from the Networking page.
## Node-state checks and graceful degradation
Six of the 30 rules require live Docker state to run: five are in the Node state category (external networks and volumes, new-resource notices, and container_name collision) and one is "Host port is already in use" in Port conflicts. All six are skipped when the Docker daemon is unreachable.
Six of the 32 rules require live Docker state to run: five are in the Node state category (external networks and volumes, new-resource notices, and container_name collision) and one is "Host port is already in use" in Port conflicts. All six are skipped when the Docker daemon is unreachable.
When the daemon is unreachable:
@@ -184,13 +199,15 @@ A partial run (where node-state checks were skipped) is stored and displayed lik
## Health-Gated Updates
The last stored preflight result feeds directly into the [Health-Gated Updates](/features/health-gated-updates) readiness check. When you trigger **Update** from a stack, Sencho opens a readiness dialog before pulling anything. The dialog reads your most recent preflight result:
The last stored preflight result feeds directly into the [Health-Gated Updates](/features/health-gated-updates) readiness check as one of several signals. When you trigger **Update** from a stack, Sencho opens a readiness dialog before pulling anything. The dialog reads your most recent preflight result (active findings only; acknowledged findings are excluded):
- A blocker finding sets the update verdict to **Blocked**.
- One or more high-risk findings set the verdict to **Review required**.
- Preflight that has never run is noted in the dialog without dragging the verdict down.
- A blocker finding sets the update verdict to **blocked**.
- A model that failed to render, or one or more high-risk findings, sets the verdict to **review required**.
- Warning-only findings do not block the verdict on their own, but keep it from reading a clean **ready**; combined with an otherwise clean update they surface as **ready with warnings**.
- A clean pass (no active findings, or only info-level ones) contributes a positive signal toward **ready**.
- Preflight that has never run is noted in the dialog as **unknown** for that signal, without dragging the overall verdict down by itself.
Running preflight before updating gives the readiness check the most accurate signal. A clean pass after resolving findings contributes a positive readiness signal for that stack.
Other signals (current container health, the pending image change, the rollback backup, and disk space) are combined with this one; the dialog's overall verdict reflects the worst signal across all of them, not preflight alone. Running preflight before updating gives the readiness check the most accurate signal for this stack.
## Running preflight
@@ -228,7 +245,7 @@ Acknowledgements are stored per node, stack, rule, and service. They do not sync
<Frame>
<img
src="/images/compose-doctor/compose-doctor-findings.png"
alt="The Doctor tab showing an amber high-risk summary card with counts for high-risk and warning findings, followed by grouped finding rows each showing a service tag, title, explanation, suggested fix, and source file path"
alt="The Doctor tab showing an amber high-risk summary card with a dismiss X button, run time, and counts for high-risk and warning findings, followed by grouped finding rows each showing a service tag, title, explanation, suggested fix, and an acknowledge button, grouped under HIGH RISK and WARNING section headers"
/>
</Frame>
@@ -240,7 +257,8 @@ There is no tier gate: Compose Doctor is available on all plans.
## Limitations
- **Advisory only.** Compose Doctor never blocks a deploy or changes any stack configuration. Act on findings or ignore them; the choice is always yours.
- **Advisory only, with one exception.** Compose Doctor's report never blocks a deploy or changes any stack configuration; act on findings or ignore them. The one exception is the self-management guard: Sencho refuses generic deploy, update, stop, down, and delete actions on its own compose project regardless of whether that finding is acknowledged. See [Self-management](#self-management).
- **Dismissing the summary is local to your browser.** The **X** on the summary card and the Doctor tab dot share a per-stack, per-node dismissal stored in your browser's local storage. It is not synced across devices or between teammates, so a dismissal you make is invisible to anyone else looking at the same stack.
- **Bind-mount checks are scoped.** Only paths that resolve inside the node's Compose base directory can be checked for existence and ownership. Absolute host paths outside that directory (such as `/mnt/media`) are not reported as missing.
- **Healthcheck rule cannot see image-level healthchecks.** The no-healthcheck rule fires when the Compose model does not declare a healthcheck. Many images define one internally that Sencho cannot see from the rendered model; treat the finding as a prompt to confirm the image provides one.
- **One run stored per node.** There is no history. Each new run overwrites the previous one for that stack on that node.
@@ -278,6 +296,12 @@ There is no tier gate: Compose Doctor is available on all plans.
<Accordion title="I want to see who ran preflight last">
The summary card shows the username and relative time: "ran 5 minutes ago by admin". This reflects the most recent run stored for this stack on the currently active node.
</Accordion>
<Accordion title="The report warns that this stack is the running Sencho instance">
Sencho detected that the stack you are checking is its own compose project. Generic deploy, update, stop, down, and delete actions are refused for this stack no matter what the preflight report says, because any of them could recreate or remove the dashboard you are using right now. Update Sencho from **Fleet -> Node Update** instead. See [Self-management](#self-management).
</Accordion>
<Accordion title="I dismissed a finding but it still shows on another machine or for a teammate">
Dismissing the summary card (the **X** button) is stored in your browser's local storage, keyed to the stack and node. It does not sync across devices, browsers, or user accounts, so a dismissal on your machine has no effect on what anyone else sees.
</Accordion>
</AccordionGroup>
## Related features
+9 -7
View File
@@ -7,6 +7,8 @@ The **Networking** tab in the right-hand **Anatomy** panel answers two questions
The tab is read-only with respect to stack deployments: it never alters a container, a Compose file, or an existing network attachment. Two write operations are available: setting the stack's *exposure intent* (stored separately so Sencho can flag mismatches over time) and creating a new Docker network on the active node, which is available to admins and covered in [Creating networks](#creating-networks) below.
This tab stays stack-scoped: it models one deployment at a time. For a node-wide inventory of every Docker network, an interactive topology graph, and cross-stack findings, see the [Networking](/features/networking) page instead. A **view node networking** link at the bottom of this tab jumps there directly.
Compose Networking reads structure: network names, service-to-network membership, published ports, network modes, and the *names* of environment variables and labels, never their values. A secret injected through `environment:` or `env_file:` never appears in the view or the logs. One caveat: a structural field assembled by interpolating a secret (a network `name:`, a published port, or an `extra_hosts` entry built from `${VAR}`) is resolved before the inspector reads it, so its value does show. Keep secrets in `environment:` or `env_file:` rather than interpolating them into structural fields. See [Environment and Secrets Guardrails](/features/environment-guardrails).
<img
@@ -26,7 +28,7 @@ The tab appears only when the active node reports that it supports Compose Netwo
<Frame>
<img
src="/images/compose-networking/networking-tab-location.png"
alt="The Anatomy panel tab strip showing ANATOMY, ACTIVITY, DOSSIER, DRIFT, ENVIRONMENT, NETWORKING (active and underlined), DOCTOR, STORAGE tabs alongside FILES and EDIT action buttons"
alt="The Anatomy panel tab strip scrolled to show DRIFT, ENVIRONMENT, COMPOSE LABELS, NETWORKING (active, highlighted), DOCTOR with an unread-findings dot, and STORAGE tabs, alongside FILES and EDIT COMPOSE action buttons"
/>
</Frame>
@@ -65,7 +67,7 @@ Exposure intent is how you tell Sencho what a stack *should* be reachable from,
| **temporary** | A short-lived exposure (a label for tracking only, not a functional configuration). |
| **unknown** | Not yet classified. |
**How inheritance works:** a service with no intent of its own inherits the stack's. In the per-service row, the **inherit** pill is selected when no override is set; an arrow indicator shows the intent the service inherits from the stack. Clearing a service's override returns it to **inherit**. Clearing the stack's intent returns it to unclassified.
**How inheritance works:** the stack row's pill options end in **unset**, which clears the stack-level classification entirely. The per-service row's pills end in **inherit** instead: a service with no override of its own takes on the stack's intent, shown by an arrow indicator next to the inherit pill (for example, `→ reverse-proxy`). Selecting inherit on a service clears its override; selecting unset on the stack clears the stack's classification.
**Permissions:** editing the intent requires stack edit access. With read-only access you can see the current classification but not change it.
@@ -171,7 +173,7 @@ When Compose Doctor is available on the active node, a prompt at the bottom of t
See [Compose Doctor](/features/compose-doctor) for the complete check set and severity levels.
A **View node networking** link near the bottom of the tab opens the node-wide [Networking](/features/networking) page, for when you want to see this stack's networks in the context of every other stack sharing them.
A **view node networking** link near the bottom of the tab opens the [Networking](/features/networking) operator page for the active node, scoped to node-wide inventory, topology, and findings rather than this one stack, for when you want to see this stack's networks in the context of every other stack sharing them.
## Creating networks
@@ -182,7 +184,7 @@ A **View node networking** link near the bottom of the tab opens the node-wide [
/>
</Frame>
Admins can create a new Docker network from the stack **Networking** tab or from the node [Networking](/features/networking) operator page using the **create network** button in the panel header.
Admins can create a new Docker network directly from the Networking tab using the **create network** button in the panel header. This is the same dialog available from the node-wide [Networking](/features/networking) operator page.
| Field | Required | Description |
|-------|----------|-------------|
@@ -236,15 +238,15 @@ After the network is created, the Networking tab refreshes automatically. The ne
## Related
<CardGroup cols={2}>
<Card title="Networking" icon="network-wired" href="/features/networking">
The node-wide operator page: full network inventory, an interactive topology graph, and cross-stack findings, independent of which stack they belong to.
</Card>
<Card title="Compose Doctor" icon="stethoscope" href="/features/compose-doctor">
Runs preflight checks against the same effective model, including the five exposure-aware findings that rely on the intents set in the Networking tab.
</Card>
<Card title="Drift Detection" icon="code-compare" href="/features/stack-drift">
Shows the same four runtime drift types with a persistent history, so you can see when drift first appeared and when it cleared.
</Card>
<Card title="Resources Hub" icon="network-wired" href="/features/resources">
Host-wide view of every Docker network and container attachment on the node, independent of which stack they belong to.
</Card>
<Card title="Environment and Secrets Guardrails" icon="shield-halved" href="/features/environment-guardrails">
Documents which fields Sencho reads and which it redacts, and how to keep secrets safe from interpolation into structural fields.
</Card>
+4 -4
View File
@@ -15,7 +15,7 @@ Click any stack in the left sidebar, then switch to the **Storage** tab in the A
<Frame>
<img
src="/images/compose-storage/storage-tab.png"
alt="The Anatomy panel with the Storage tab selected, showing the STORAGE PORTABILITY label, a node-bound verdict card with the reason that a bind path outside the stack directory must exist on every target node, and a SNAPSHOT COVERAGE section below"
alt="The Anatomy panel tab strip with Storage selected, showing the STORAGE PORTABILITY label, a node-bound verdict card with the reason that a bind path outside the stack directory must exist on every target node, the mount row for that bind, and a SNAPSHOT COVERAGE section with a warning and Take a fleet snapshot link"
/>
</Frame>
@@ -55,7 +55,7 @@ A few things worth knowing:
<Frame>
<img
src="/images/compose-storage/storage-node-bound.png"
alt="The Storage panel for a stack that mounts the Docker socket, showing a node-bound verdict card with the reason 'Service mounts the Docker socket, tying the stack to this host's Docker engine', followed by the socket mount row with SOCKET, RW, and external chips and the path /var/run/docker.sock"
alt="The Storage panel for a stack that mounts the Docker socket, showing a node-bound verdict card with the reason the service mounts the Docker socket tying the stack to this host's Docker engine, followed by the socket mount row with SOCKET, RW, and external chips and the path /var/run/docker.sock"
/>
</Frame>
@@ -113,8 +113,8 @@ See [Compose Doctor](/features/compose-doctor) for the full rule reference and h
<Card title="Fleet Snapshots" icon="camera" href="/features/fleet-backups">
Back up Compose and env files across all nodes in one action. Covers configuration, not volume data.
</Card>
<Card title="Files and Volumes" icon="folder-open" href="/features/stack-file-explorer">
Browse and edit the files inside a stack directory and read the contents of named volumes.
<Card title="Stack File Explorer" icon="folder-open" href="/features/stack-file-explorer">
Browse and edit the files inside a stack's directory and its mounted volumes from the Files tab.
</Card>
<Card title="Environment Guardrails" icon="key" href="/features/environment-guardrails">
Keep secrets out of mount paths so they are not exposed in the storage inventory.
+20 -8
View File
@@ -21,10 +21,10 @@ Badge counts in the Resources Hub continue to reflect the raw findings. Suppress
## Creating a suppression
Open the **Security** page → **Suppressions** tab, then click **Add Suppression**.
Open the **Security** page → **Suppressions** tab, then click **Add suppression**. The same tab also holds a **Misconfig acknowledgements** panel for Compose-config findings; that is a related but separate feature, covered in [Misconfig acknowledgements](/features/vulnerability-scanning#misconfig-acknowledgements).
<Frame>
<img src="/images/cve-suppressions/settings-panel.png" alt="Security page Suppressions tab with the CVE Suppressions panel listing two accepted CVEs, each showing the CVE ID, an outlined package badge, a clamped reason line, the author and expiry metadata, and a trash icon for removal" />
<img src="/images/cve-suppressions/settings-panel.png" alt="Security page Suppressions tab with the CVE Suppressions panel listing two rules: one CVE tagged Accepted risk with a package image-pattern badge and a reason, and one GHSA tagged Not affected with an image-pattern badge, a reason, and a Justification line. Each row has edit and delete icons, with the Misconfig acknowledgements panel visible below." />
</Frame>
The dialog has the following fields:
@@ -40,7 +40,7 @@ The dialog has the following fields:
| **Expires in (days, optional)** | Number of days after which the suppression stops applying. Useful for "patched in the next release" entries. Leave blank for an indefinite suppression. |
<Frame>
<img src="/images/cve-suppressions/create-dialog.png" alt="New suppression dialog with kicker SUPPRESSIONS · NEW, title New suppression, and the form fields populated with example values for a Git CVE scoped to LinuxServer images" />
<img src="/images/cve-suppressions/create-dialog.png" alt="New suppression dialog with kicker SUPPRESSIONS · NEW, title New suppression, and the form filled in for a GHSA scoped to a LinuxServer image, with the Triage decision set to Not affected and the resulting OpenVEX justification field set to Vulnerable code not present" />
</Frame>
### Suppressing directly from a scan result
@@ -63,7 +63,7 @@ The winning rule's Reason field is the one displayed on the row.
Open any scan drawer and look at the Vulnerabilities table. Suppressed rows are dimmed and carry a shield-off icon next to the CVE ID:
<Frame>
<img src="/images/cve-suppressions/suppressed-row.png" alt="Vulnerabilities tab of a Sencho image scan showing two dimmed suppressed rows for Docker CVEs at the top with the shield-off icon, and one un-suppressed GHSA row below at full brightness with an inline Suppress button on the right" />
<img src="/images/cve-suppressions/suppressed-row.png" alt="Vulnerabilities tab of a Sencho image scan showing one dimmed suppressed CVE row with a small shield icon next to the CVE ID, contrasted against the full-brightness unsuppressed rows above and below it" />
</Frame>
The same dim-and-icon treatment carries through to the **Compare** sheet, so a CVE you've already accepted does not look like a new regression when comparing against an older baseline. Hovering the package column of a suppressed row reveals the recorded Reason without expanding the row.
@@ -74,7 +74,11 @@ Suppressions are managed on the **control** Sencho instance and replicate automa
- Creating, editing, or removing a suppression on the control pushes the full list to every remote.
- A Sencho instance that has received at least one push from a control is a **replica**. On a replica, the **Security** page → **Suppressions** tab shows the suppression list read-only, and replicated rows carry a small `replicated` badge so they are easy to tell apart from any locally-created entries.
- When you're signed into a control and have a **remote node selected** from the node switcher, the CVE Suppressions panel itself is hidden and a "Scanner is per-node" banner explains that scanning runs on the remote while rules live on the control.
- Suppressions and misconfig acknowledgements are always managed on the instance you are signed into directly, never proxied through the node switcher. If you're signed into one Sencho instance and pick a **remote node** from the node switcher, the Suppressions tab hides both panels and shows a "Managed on the local instance" notice instead, telling you to switch to that node's own URL to view or edit its rules.
<Frame>
<img src="/images/cve-suppressions/remote-node-banner.png" alt="Security page Suppressions tab with a remote node selected from the node switcher, showing a Managed on the local instance notice instead of the suppression panels" />
</Frame>
The full replication, retry, and reanchor flow (including the API call to re-bind a replica to a new control) is documented in [Fleet Sync](/features/fleet-sync).
@@ -86,12 +90,20 @@ To have an accepted CVE stop counting toward the gate, an admin can enable **Hon
The toggle is off by default and is set per Sencho instance, because the gate runs on whichever instance performs the deploy.
## Editing a suppression
Click the pencil icon on any row you created (not visible on `replicated` rows viewed from a replica) to open the same form pre-filled. The CVE or advisory ID and Package are identity fields and stay fixed; the Image pattern, Triage decision, OpenVEX justification, Reason, and Expiry are all editable.
<Frame>
<img src="/images/cve-suppressions/edit-dialog.png" alt="Edit suppression dialog with the CVE or advisory ID and Package fields disabled and greyed out, and the Image pattern field focused, showing the Save changes button" />
</Frame>
To change the CVE, advisory ID, or package a rule targets, remove the existing rule and create a fresh one instead; those two fields cannot be edited in place.
## Removing a suppression
Click the trash icon on any row in the panel. A confirmation dialog ("Remove suppression", kicker `SUPPRESSIONS · REMOVE · IRREVERSIBLE`) warns that future scan results will surface the CVE again wherever it applies.
To change a suppression's scope (for example, to narrow an image pattern or extend the expiry), remove the existing rule and create a fresh one with the updated fields.
## Export awareness
Suppressed findings carry through to the [SARIF export](/features/vulnerability-scanning#sarif-export) with a SARIF `suppressions` entry of `kind: external` and `status: accepted`. Code-scanning dashboards that respect SARIF suppressions dismiss those findings with the Reason you recorded.
@@ -111,7 +123,7 @@ You can also export the full set of triage decisions as an **OpenVEX** document
Replication runs on every write. If the push failed (network blip, replica restart), the control retries every 5 minutes for 24 hours and the replica picks up the latest state on the next successful push. See [Fleet Sync](/features/fleet-sync) for how to investigate persistent push failures.
</Accordion>
<Accordion title="I see suppressions on a replica but cannot edit them">
Replicas are read-only for security rules. Sign in to the control instance to add, edit, or delete suppressions; changes sync automatically. Replicated rows show a `replicated` badge and no trash icon.
Replicas are read-only for security rules. Sign in to the control instance to add, edit, or delete suppressions; changes sync automatically. Replicated rows show a `replicated` badge and no edit or trash icon.
</Accordion>
<Accordion title="A suppression does not match a finding I expect it to">
Two common causes:
+41 -27
View File
@@ -6,7 +6,7 @@ description: Real-time system health, stack load, configuration overview, fleet
The **Home** tab is the first thing you see after logging in. It surfaces the active node's overall health, live system metrics, the load on every stack, the on/off state of every automation and security feature, a fleet- or restart-activity panel, and the running alert tape, all on a single scrollable page.
<Frame>
<img src="/images/dashboard/dashboard-overview.png" alt="Sencho Home dashboard showing the Critical state masthead, four-tile resource gauge strip, paginated stack health table, Configuration Status card paired with Fleet Heartbeat, and the Recent Alerts feed." />
<img src="/images/dashboard/dashboard-overview.png" alt="Sencho Home dashboard showing the Healthy state masthead with its left-edge accent rail, four-tile resource gauge strip, paginated stack health table with Source and Port columns, Configuration Status card paired with Fleet Heartbeat, and the Recent Alerts feed." />
</Frame>
## Status masthead
@@ -14,17 +14,17 @@ The **Home** tab is the first thing you see after logging in. It surfaces the ac
The masthead at the top of the dashboard is the single place to read the node's current condition.
<Frame>
<img src="/images/dashboard/status-masthead.png" alt="Status masthead in the Critical state with the editorial state word, the meta line 'LOCAL · 4 NODES · LAST SYNC 1S', the reasons line 'RAM 94% · 18 unread errors', the RUNNING / CPU / MEM stat tiles, and a 92-alerts counter." />
<img src="/images/dashboard/status-masthead.png" alt="Status masthead in the Healthy state with a teal left-edge accent rail, the editorial state word, the meta line 'LOCAL · 4 NODES · LAST SYNC 0S', the reasons line 'All systems nominal', the RUNNING / CPU / MEM stat tiles, and an 8-alerts counter." />
</Frame>
It carries:
- A **state word** (Healthy, Degraded, or Critical) set in the editorial display face so the reader sees it first.
- A **pulsing dot** that mirrors the state color: green when nominal, amber when degraded, rose when critical. The dot is solid (no pulse) when Healthy.
- A thin **accent rail** down the card's left edge that mirrors the state color: brand teal when Healthy, amber when Degraded, rose when Critical. The rail shimmers when Healthy and glows steadily otherwise.
- A **meta line** in uppercase mono tracking with the active node's name, the number of nodes registered to this Sencho instance, and the time since the last successful poll, for example `LOCAL · 4 NODES · LAST SYNC 1S`.
- A **reasons line** that names exactly which signals moved the state away from Healthy, for example `RAM 95% · 18 unread errors`. When the state is Healthy the reasons line reads `All systems nominal`.
- Three quick stat tiles on the right edge of the bar: **RUNNING** (`active/total`), **CPU**, and **MEM**.
- An alerts counter pinned to the far right, showing the number of unread notifications next to a bell icon. The icon and count tint amber while at least one alert is unread.
- Three quick stat tiles on the right edge of the bar, hidden below the `md` breakpoint: **RUNNING** (`active/total`), **CPU**, and **MEM**.
- An alerts counter pinned to the far right, showing the number of unread notifications next to a bell icon and the word `alert` or `alerts`. The bell and count tint amber while at least one alert is unread.
The masthead's CPU stat tile tints amber once host CPU crosses 80% and stays amber even at 90% or higher. The MEM and RUNNING values stay neutral; the reasons line and the gauge strip below are where you read severity.
@@ -33,7 +33,7 @@ The masthead's CPU stat tile tints amber once host CPU crosses 80% and stays amb
A single rail of four tiles shows the numbers that change minute-to-minute.
<Frame>
<img src="/images/dashboard/resource-gauges.png" alt="Four-tile resource gauge strip: a CPU hero tile at 0.3% with the 'avg 0% last 10m · peak 0% @ 03:05 PM' caption and a 10-minute sparkline; a MEMORY tile at 94% with the gauge bar in destructive red and '14.7 GB / 15.6 GB' below; a DISK tile at 57% in brand cyan with '93.6 GB / 173.6 GB'; and a NETWORK tile reading '8.8 KB/s' with the rx/tx split and a rhythm sparkline." />
<img src="/images/dashboard/resource-gauges.png" alt="Four-tile resource gauge strip: a CPU hero tile at 0.4% with the 'avg 0% last 10m · peak 0% @ 08:05 PM' caption and a 10-minute sparkline; a MEMORY tile at 15% in brand cyan with '2.4 GB / 15.6 GB' below; a DISK tile at 59% in brand cyan with '165.6 GB / 291.7 GB'; and a NETWORK tile reading '3.3 KB/s' with the rx/tx split and a rhythm sparkline." />
</Frame>
| Tile | What it shows |
@@ -56,20 +56,22 @@ While the dashboard is loading the CPU tile reads `--` and the caption shows `co
A mono table of every stack discovered in the active node's `COMPOSE_DIR`, sorted so the stacks demanding attention sit at the top.
<Frame>
<img src="/images/dashboard/stack-health.png" alt="Stack health table titled 'Stack health · 15 STACKS · SORTED BY LOAD' with pagination chevrons reading 1 / 2, listing eight stacks (profilarr, cloudflared, tautulli, swag, plex, bazarr, radarr, prowlarr) with a green status dot, host 'Local', uptime, current CPU and MEM, and a 10-minute CPU sparkline per row." />
<img src="/images/dashboard/stack-health.png" alt="Stack health table titled 'Stack health · 15 STACKS · SORTED BY LOAD' with pagination chevrons reading 1 / 2 and STACK / SOURCE / PORT / UP / CPU / MEM / CPU · 10m column headers, listing eight stacks (cloudflared, swag, plex, bazarr, radarr, sonarr, prowlarr, tautulli) each with a row tint, Local source, published port, uptime, current CPU and MEM, and a 10-minute CPU sparkline." />
</Frame>
The row itself carries the health tint (see below); there is no separate status-dot column. The columns are:
| Column | Description |
|--------|-------------|
| **Status dot** | Green when the stack is running and its 10-minute peak CPU is under 80%, amber when peak CPU is at or above 80%, rose when any container has exited or peak CPU is at or above 90% |
| **STACK** | Stack name with an orange update badge when a newer image has been detected. When per-service status is known, the badge names the outdated service (or a count for several). The badge appears regardless of the sidebar indicator setting. |
| **HOST** | Active node this stack belongs to |
| **UP** | How long the oldest running container has been up, in compact units (`s` / `m` / `h` / `d`); a stopped or never-started stack reads `--` |
| **CPU** | Latest aggregate CPU across the stack's containers |
| **MEM** | Latest aggregate memory across the stack's containers, formatted in MB or GB |
| **STACK** | Stack name with an orange "Update available" badge when a newer image has been detected. When per-service status is known, the badge narrows to the outdated service name or a count (for example `2 updates`); hover for the full breakdown. The badge appears regardless of the sidebar indicator setting. Sortable. |
| **SOURCE** | `Git` when the stack is linked to a [Git source](/features/git-sources), `Local` otherwise |
| **PORT** | The stack's main published port, or `--` when it does not publish one |
| **UP** | How long the oldest running container has been up, in compact units (`s` / `m` / `h` / `d`); a stopped or never-started stack reads `--`. Sortable. |
| **CPU** | Latest aggregate CPU across the stack's containers. Sortable. |
| **MEM** | Latest aggregate memory across the stack's containers, formatted in MB or GB. Sortable. |
| **CPU · 10m** | Per-stack 10-minute sparkline tinted to match the row state; warn and error rows mark the peak with a contrasting accent color |
Rows sort first by state (errors → warnings → healthy) and then by 10-minute peak CPU descending, so an exited stack always rises to the top and the noisiest healthy stacks float above the quiet ones. Warning rows pick up a subtle amber wash; critical rows pick up a rose wash. Click any row, or focus it and press <kbd>Enter</kbd> or <kbd>Space</kbd>, to jump to that stack's editor.
By default, rows sort by state (errors → warnings → healthy) and then by 10-minute peak CPU descending, so an exited stack always rises to the top and the noisiest healthy stacks float above the quiet ones (the header reads `sorted by load`). Click the **STACK**, **UP**, **CPU**, or **MEM** column header to sort by that column instead; the `sorted by load` label disappears once you do. Click the same header again to flip between ascending and descending. A row is tinted amber when its 10-minute peak CPU is at or above 80% or the stack is partially down (some but not all containers exited), or rose when the stack is fully exited or its peak CPU is at or above 90%. Click any row, or focus it and press <kbd>Enter</kbd> or <kbd>Space</kbd>, to jump to that stack's editor.
The table paginates at eight rows; chevrons appear in the header along with an `N / M` indicator when there is more than one page. When the active node has no stacks at all the card renders an empty state with a layered-disks glyph and the message `No stacks found. Create one from the sidebar.`
@@ -78,7 +80,7 @@ The table paginates at eight rows; chevrons appear in the header along with an `
The Configuration Status card is the at-a-glance audit of every toggleable automation and security feature on the active node, so nothing is silently off when you expect it to be on.
<Frame>
<img src="/images/dashboard/configuration-status.png" alt="Configuration Status card with four sections: Notifications (Notification agents, Alert rules, Notification routing all reading None), Automation (Auto-heal None, Auto-update '1 / 1', Webhooks None, Scheduled tasks '2 actives'), Security (MFA Off, SSO Off, Vulnerability scanning None), and Backups & Thresholds (Recovery Vault active, Alert thresholds 'CPU 100% · RAM 100% · Disk 100%', Crash detection On)." />
<img src="/images/dashboard/configuration-status.png" alt="Configuration Status card with four sections: Notifications (Channels 'Discord', Alert rules None, Routing None, Mute Rules None), Automation (Auto-heal policies '1 / 1 active', Auto-update schedules '1 / 1 active', Webhooks None, Scheduled tasks '1 active'), Security (MFA Off, SSO Off, Trivy installed Installed, Scan policies None), and Backups & Thresholds (Recovery Vault active, Alert thresholds Off, Crash detection On)." />
</Frame>
The card is divided into four sections.
@@ -87,9 +89,10 @@ The card is divided into four sections.
| Row | What it shows |
|-----|---------------|
| **Notification agents** | The list of enabled delivery agents from `Discord`, `Slack`, `Webhook`, and `Apprise`, joined by commas; reads `None` when no agent is enabled |
| **Channels** | The list of enabled delivery agents from `Discord`, `Slack`, `Webhook`, and `Apprise`, joined by commas; reads `None` when no agent is enabled |
| **Alert rules** | Total per-stack alert rules in effect, formatted `<n> rules` |
| **Notification routing** | Number of enabled routing rules that direct categories to specific agents, formatted `<n> routes` |
| **Routing** | Number of enabled routing rules that direct categories to specific agents, formatted `<n> routes` |
| **Mute Rules** | Number of enabled notification-suppression rules, formatted `<n> rules`. See [Mute Rules](/features/alerts-notifications#mute-rules). |
### Automation
@@ -106,7 +109,8 @@ The card is divided into four sections.
|-----|---------------|
| **MFA** | `On` when TOTP is configured for the signed-in operator, `Off` when configured but disabled, `Not set up` when there is no MFA secret on file |
| **SSO** | The active SSO provider name (`OIDC`, `Google`, `GitHub`, `Okta`, `LDAP`); reads `Off` when SSO is not enabled |
| **Vulnerability scanning** | Count of enabled scan policies on the active node; reads `None` when no policy is enabled |
| **Trivy installed** | `Installed` when a Trivy scanner binary is available on this node, `Not installed` otherwise |
| **Scan policies** | Count of enabled vulnerability scan policies on the active node, formatted `<n> policies`; reads `None` when no policy is enabled |
### Backups & Thresholds
@@ -116,21 +120,21 @@ The card is divided into four sections.
| **Alert thresholds** | The current host thresholds, formatted `CPU x% · RAM y% · Disk z%`, or `Off` when host threshold alerts are disabled |
| **Crash detection** | `On` when global container-crash notifications are enabled, `Off` otherwise |
Click any row to jump directly to the settings section that manages it. Rows that require a higher tier than the active license are not rendered. The section headers (Notifications, Automation, Security, Backups & Thresholds) only render when at least one of their rows is visible.
Click any row to jump directly to the settings section that manages it.
The data refreshes automatically every 60 seconds and immediately on any container start/stop/restart event broadcast over the live notification stream, so the card stays in lockstep with what the rest of the UI shows.
The card refreshes automatically every 60 seconds. It also refreshes immediately whenever a scheduled task is created, edited, toggled, or deleted, so the **Scheduled tasks** row (and the rest of the card) reflects that change within a second. Other changes covered by this card, such as switching the cloud backup provider or enabling SSO, update on the next 60-second tick.
## Activity panel
To the right of Configuration Status, the dashboard shows one of two activity panels depending on whether you have remote nodes registered:
To the right of Configuration Status, the dashboard shows one of two activity panels:
- **Fleet Heartbeat** when at least one remote node is configured.
- **Stack Restarts (7d)** when this Sencho instance manages only the local node.
- **Fleet Heartbeat** when at least one remote node is registered and your role has the `node:read` permission.
- **Stack Restarts (7d)** when this Sencho instance manages only the local node, or when your role lacks `node:read` (so a deployer-level role never sees a fleet card it cannot load).
### Fleet Heartbeat
<Frame>
<img src="/images/dashboard/fleet-heartbeat.png" alt="Fleet Heartbeat card with the radio-tower kicker '4 NODES' on the right, listing four entries with status dots: Local with the brand 'LOCAL' pill and 16 containers; Opsix with 6 containers and 91 ms latency; Pitt-Moba with 5 containers and 52 ms latency; sencho-pilot-test with no container count and 'n/a' latency." />
<img src="/images/dashboard/fleet-heartbeat.png" alt="Fleet Heartbeat card with the radio-tower kicker '4 NODES' on the right, listing four entries with green status dots: Local with the brand 'LOCAL' pill and 16 containers; Opsix with 4 containers and 75 ms latency; Pitt-Moba with 3 containers and 65 ms latency; SLX-Mars with 4 containers and 243 ms latency." />
</Frame>
The header carries a radio-tower icon, the total node count, and a `<n> unreachable` callout in destructive red whenever any node is offline. Each row shows:
@@ -159,7 +163,7 @@ Stacks with zero restarts are not listed individually; they are summarized in a
The bottom-most card on the dashboard collects every triggered notification on the active node, sorted by recency.
<Frame>
<img src="/images/dashboard/recent-alerts.png" alt="Recent Alerts card with pagination chevrons reading 1 / 12, listing eight rows tagged 'sencho-pilot-test' with severity icons (warning triangle for the threshold breach, info circles for deploys, destructive octagons for the two crash-detected entries), each carrying a relative timestamp, and a 'Clear All Notifications' button anchored at the bottom right." />
<img src="/images/dashboard/recent-alerts.png" alt="Recent Alerts card listing eight local, unbadged info-level rows about stack image updates and auto-update runs for plex, radarr, and swag, each carrying a relative timestamp such as '4h ago', with a 'Clear All Notifications' button anchored at the bottom right. No pagination chevrons appear because the feed holds exactly eight entries." />
</Frame>
Each row shows:
@@ -173,7 +177,17 @@ A **Clear All Notifications** button is anchored at the bottom right of the card
The list paginates at eight rows; chevrons and an `N / M` indicator appear in the header when there is more than one page.
Alert rules and severity routing are configured per stack in **Settings · Notifications · Channels**. See [Alerts and notifications](/features/alerts-notifications) for channel setup, per-stack rules, and the routing engine.
Per-stack alert rules live on each stack's **Monitor** sheet; delivery channels and severity routing are configured in **Settings · Notifications**. See [Alerts and notifications](/features/alerts-notifications) for channel setup, per-stack rules, mute rules, and the routing engine.
## On a phone
<Frame>
<img src="/images/dashboard/dashboard-mobile.png" alt="Mobile dashboard masthead reading 'Healthy' with a status dot, node switcher, and '15 stacks · 15 up · 0 dn · sync 0s' meta line, followed by a CPU hero card, a mem/disk/net three-up strip, and a Stack Health list of six rows with a 'view all' link." />
</Frame>
The phone layout condenses the dashboard to what fits a single thumb-scroll: the masthead (state word, status dot, and a compact `<n> stacks · <n> up · <n> dn · sync <age>` meta line), the CPU hero card with its sparkline, a three-up memory/disk/network strip, and a **Stack Health** list capped at six rows with a **view all →** link to the full stack list. Rows show only the stack name, its host, a 10-minute sparkline, and current CPU; tap a row to open that stack's editor.
Configuration Status, the Fleet Heartbeat / Stack Restarts panel, and Recent Alerts are desktop-only; open Sencho on a wider screen to see them. Notifications stay reachable from the bell icon in the mobile masthead.
## How health is derived
@@ -201,7 +215,7 @@ The dashboard is built from several independent polling loops so that fast-movin
| Stack Restarts (7d) | 5 minutes |
| Recent Alerts | Pushed live over the notifications WebSocket, with a 60-second safety-net reconcile poll |
In addition, every Docker container event (start, stop, die, restart, health-status change) on the active node fires a `sencho:state-invalidate` browser event that immediately refetches container counts, system stats, stack statuses, and the Configuration Status card so the masthead and the table react in well under a second instead of waiting for the next polling tick.
In addition, every Docker container event (start, stop, die, restart, health-status change) on the active node fires a `sencho:state-invalidate` browser event that immediately refetches container counts, system stats, and stack statuses, so the masthead, gauges, and stack health table react in well under a second instead of waiting for the next polling tick. The Configuration Status card ignores these container-level events; it only reacts early to scheduled-task changes (see the section above), so it stays on its 60-second cadence for everything else.
When you switch the active node from the node switcher, the dashboard resets every panel to its loading state and starts a new round of fetches against the new node so you never see stale data from the previous one.
+20 -8
View File
@@ -3,7 +3,7 @@ title: Deep links and URLs
description: Bookmark, share, and refresh Sencho views with real browser URLs that survive Back and Forward.
---
Every major screen in Sencho now has a stable address in the browser bar. Refresh the page, paste a link to a teammate, or use Back and Forward without losing your place.
Every major screen in Sencho has a stable address in the browser bar. Refresh the page, paste a link to a teammate, or use Back and Forward without losing your place.
## What gets its own URL
@@ -16,27 +16,36 @@ Sencho encodes the active node, the view you are on, and the deep state that vie
| Stack detail | `/nodes/local/stacks/radarr` | Radarr's stack detail (anatomy). Sidebar entry point |
| Compose editor | `/nodes/local/stacks/radarr/compose` | Radarr's compose.yaml Monaco editor |
| Env tab + file | `/nodes/local/stacks/radarr/env?env=.env.prod` | Radarr's env tab with a specific env file selected |
| Files browser | `/nodes/local/stacks/radarr/files` | Radarr's file browser (desktop; opens the compose editor instead on a phone) |
| Resources | `/nodes/local/resources` | Resources for the active node |
| Networking | `/nodes/local/networking` | Networking operator page for the active node |
| Security tab | `/nodes/local/security/images` | Security view on the Images tab |
| App Store | `/nodes/local/templates` | App Store on the active node |
| Logs | `/nodes/local/logs` | Cross-fleet log aggregation |
| Update | `/nodes/local/updates` | Fleet-wide update check |
| Console | `/nodes/local/host-console` | Host Console, a limited-availability operator surface documented on its own page when enabled on an instance |
| Audit | `/nodes/local/audit` | Audit history |
| Settings section | `/nodes/local/settings/nodes` | Settings on the Nodes section |
| Fleet tab | `/nodes/local/fleet/snapshots` | Fleet on the Snapshots tab (desktop) |
| Schedules filter | `/nodes/local/schedules?node=3` | Scheduled operations filtered to node `3` |
Fleet, Logs, Update, Schedules, and Audit are gated by role or license tier and are hub-only: their URLs redirect to the dashboard while a remote node is active. Console is also gated, but is reachable against any node, remote included. See [Multi-Node Fleet](/features/multi-node#what-top-level-views-show-when-a-remote-node-is-active) for the hub-only list and [RBAC & User Management](/features/rbac) for the full permission matrix.
Fleet tab URLs are stable slugs that do not always match the tab's on-screen label. For example the **Status** tab uses the `configuration` segment and the **Map** tab uses the `dependencies` segment.
Remote nodes use a slug derived from the node name and id (for example `/nodes/nas-box-42/dashboard`). The default local node keeps the short `local` slug.
## Stack detail vs compose editor
Clicking a stack in the sidebar opens **stack detail** at `/nodes/<node>/stacks/<stack>` (anatomy on the right). Opening the compose, env, or files editor appends that tab to the path:
- `/stacks/radarr` detail
- `/stacks/radarr/compose` compose.yaml editor
- `/stacks/radarr/env` env editor
- `/stacks/radarr/files` file browser (desktop)
- `/stacks/radarr`: detail
- `/stacks/radarr/compose`: compose.yaml editor
- `/stacks/radarr/env`: env editor
- `/stacks/radarr/files`: file browser (desktop)
Closing the Monaco editor returns you to the detail URL for that stack.
## Env tab URLs
Sencho encodes env file selection in the `?env=` query on the Env tab only:
@@ -45,6 +54,7 @@ Sencho encodes env file selection in the `?env=` query on the Env tab only:
- **Non-default files use basenames only:** `?env=.env.prod`, never a server filesystem path.
- **Legacy bookmarks:** older links that used an absolute path in `?env=` still open the matching file by basename. Sencho normalizes the URL to basename form when the address bar updates.
- **Unknown basenames:** if `?env=` does not match any file on the stack, Sencho opens the default env file.
- **No env files:** a stack with no env file at all has no Env tab. Opening `/env` on it stays on the compose editor instead.
Compose and Files routes never carry `?env=`.
@@ -74,7 +84,9 @@ On a phone, Home and the stack list are distinct URLs even though both relate to
On desktop, opening the stack list at `/nodes/local/stacks` canonicalizes to `/nodes/local/dashboard` because the sidebar stack list is part of the home layout on a wide screen.
Settings follows the same list/detail split: `/nodes/local/settings` is the section list; `/nodes/local/settings/<section>` opens a section directly.
Settings follows a similar split, but only on a phone: `/nodes/local/settings` opens the section list, and `/nodes/local/settings/<section>` opens a section directly. On desktop, `/nodes/local/settings` always normalizes straight to `/nodes/local/settings/appearance`; there is no bare section-list state to land on.
Fleet, Resources, Security, App Store, Logs, Update, Audit, and Schedules keep the exact same URL between desktop and phone, each rendering a phone-optimized screen at that path. Console also keeps the same URL on a phone, reflowing the terminal rather than switching to a dedicated phone screen.
On a phone, `/nodes/local/stacks/<stack>/files` opens the compose editor instead. Sencho does not expose a separate file-browser URL on a phone.
@@ -86,6 +98,6 @@ Some in-app state is intentionally not encoded:
## Tips
- Use the global search palette (<kbd>Ctrl</kbd>+<kbd>K</kbd>) as today; navigation still updates the URL when you pick a page, node, or stack.
- Use the global search palette (<kbd>Ctrl</kbd>+<kbd>K</kbd>) to jump anywhere; navigation still updates the URL when you pick a page, node, or stack.
- Deep links use stack directory names (the name you see in the sidebar), not internal compose filenames.
- Opening Sencho at `/` sends you to the active node's dashboard once you are signed in.
+7 -3
View File
@@ -10,6 +10,8 @@ Deploy enforcement is the pre-flight half of Sencho's vulnerability workflow. Wh
Deploy enforcement and scan policies are available on every tier.
</Note>
The **Security** page's Overview tab carries a **Deploy enforcement** summary card showing the count of enabled block policies and the current Honor suppressions state. The card is read-only; manage policies from the **Policies** tab.
## Configuring a block policy
Policies are managed on the **Security** page → **Policies** tab. The **Add policy** button opens the editor. When no policies exist, an empty-state callout reads "No scan policies configured" with a prompt to add one. Existing policies appear as a list of cards: each card shows the policy name, a badge per active block condition (`max: <SEVERITY>` when the severity threshold is on, `KEV`, and `Fixable`), a destructive `block` badge when the pre-flight gate is active, and a `disabled` badge when the policy is off. Below the name, the card shows `Scope:` followed by the stack-pattern glob in monospace, or `all stacks` in italics when no pattern is set. Pencil and trash buttons appear for admins on control nodes.
@@ -21,7 +23,7 @@ Policies are managed on the **Security** page → **Policies** tab. The **Add po
The editor exposes the fields that govern enforcement:
<Frame>
<img src="/images/deploy-enforcement/policy-edit-modal.png" alt="New policy modal with kicker SECURITY · NEW POLICY and title New policy. Fields shown: Name filled with 'Production block on critical', Stack pattern (optional) empty, Block conditions section with Severity threshold toggled ON and the Critical severity dropdown visible, Known-exploited (KEV) ON, Fixable Critical/High ON, Block on deploy OFF, Enabled ON. Cancel and Create buttons at the bottom." />
<img src="/images/deploy-enforcement/policy-edit-modal.png" alt="New policy modal with kicker SECURITY · NEW POLICY and title New policy. Fields shown: Name filled with 'Production block on critical', Stack pattern (optional) filled with 'prod-*', Block conditions section with Severity threshold toggled ON and the Critical severity dropdown visible, Known-exploited (KEV) ON, Fixable Critical/High ON, Block on deploy ON, Enabled ON. Cancel and Create buttons at the bottom." />
</Frame>
| Field | Purpose |
@@ -38,7 +40,7 @@ The editor sets the pattern, the block conditions, and the toggles. Per-node sco
### Honor suppressions
The **Honor suppressions in deploy blocks** toggle sits at the bottom of the **Policies** tab, below the policy list. When on, a [suppressed CVE](/features/cve-suppressions) no longer counts toward a block-on-deploy policy, so an accepted finding will not stop a deploy on this instance. When off (the default), policies evaluate the raw scan result and block on any finding that matches a condition, including those you have suppressed elsewhere.
The **Honor suppressions in deploy blocks** toggle sits at the bottom of the **Policies** tab, below the policy list. When on, a [suppressed CVE](/features/cve-suppressions) no longer counts toward a block-on-deploy policy, so an accepted finding will not stop a deploy on this instance. Off by default, policies block on the raw scan result, including findings you have suppressed elsewhere.
## How enforcement runs
@@ -68,8 +70,10 @@ Pre-flight scans use the same 24-hour digest cache as on-demand scans, so the se
## What the block dialog shows
This is a hard gate: the deploy is rejected and cannot proceed without an admin bypass. It is a different dialog from the optional [pre-deploy scan advisory](/features/vulnerability-scanning#pre-deploy-scan-advisory), which only shows scan results for review on a manual deploy and never blocks.
<Frame>
<img src="/images/deploy-enforcement/block-dialog.png" alt="Deploy blocked dialog with kicker PROFILARR · SCAN POLICY · BLOCKED, title 'Deploy blocked by security policy', a description naming the policy and block conditions, a violation row for santiagosayshey/profilarr:latest with 16 CRITICAL · 111 HIGH · 59 FIXABLE counts and Severity and Fixable reason badges and a CRITICAL severity chip, and Close and Deploy anyway buttons." />
<img src="/images/deploy-enforcement/block-dialog.png" alt="Deploy blocked dialog with kicker PROFILARR · SCAN POLICY · BLOCKED, title 'Deploy blocked by security policy', a description naming the policy and block conditions, a violation row for santiagosayshey/profilarr:latest with 20 CRITICAL · 150 HIGH · 73 FIXABLE counts and Severity and Fixable reason badges and a CRITICAL severity chip, and Close and Deploy anyway buttons." />
</Frame>
The dialog shows:
+20 -20
View File
@@ -1,6 +1,6 @@
---
title: Deploy Progress
description: Stream live output from stack deploy, update, install, restart, stop, and scan operations with a structured log view, a minimizable pill, and an optional inline status band.
description: Stream live output from stack deploy, update, install, restart, stop, take down, and node scan operations with a structured log view, a minimizable pill, and an optional inline status band.
---
When you trigger a stack operation that runs through `docker compose` or the Docker Engine, Sencho opens a live progress view that streams output as it happens. Each line becomes a timestamped row with a stage badge so you can track the deployment lifecycle as it runs. The progress view appears as a centered modal overlay by default or as a quiet status band on the stack detail when you prefer a less intrusive style. Either way, the view can be minimized to a floating pill that follows you across navigation so you can leave the App Store mid-install and still see the status from any screen.
@@ -24,8 +24,8 @@ The modal opens automatically when you trigger an action. It floats centered in
### What the modal shows
- **Header**: the action verb (Deploying, Updating, Installing, Restarting, Stopping, Scanning), the stack name in monospace truncated at 200 px, and an elapsed-time chip that appears once the connection moves past the initial **Connecting...** state.
- **Status indicator** in the upper right: one of `Connecting...` while the stream attaches, a live `<n> lines` counter while output is flowing, `Succeeded · closes in <n>s` after a clean finish, or the failure message itself when the run errors out. If the live stream cannot attach or drops, the indicator switches to `Live progress unavailable` and the operation keeps running in the background.
- **Header**: the action verb (Deploying, Updating, Installing, Restarting, Stopping, Taking down, Scanning), the stack name in monospace truncated at 200 px, and an elapsed-time chip that appears once the connection moves past the initial **Connecting...** state.
- **Status indicator** in the upper right: one of `Connecting...` while the stream attaches, a live `<n> lines` counter while output is flowing, `Succeeded` with a `closes in <n>s` countdown after a clean finish, or the failure message itself when the run errors out. If the live stream cannot attach or drops, the indicator switches to `Live progress unavailable` and the operation keeps running in the background.
- **Structured log body**: one row per output line. Each row carries a timestamp, a fixed-width stage badge, and the log message. Error rows have a destructive left border and tinted background so they stand out without scanning.
- **Footer**: a `Raw output` / `Hide raw` toggle on the left, with **Minimize** and **Close** buttons on the right.
@@ -48,10 +48,10 @@ When the gate passes, the success state and 4-second auto-close resume. When the
Closing or minimizing the modal never stops the observation. The gate runs to completion in the background regardless of what happens to the progress view.
The health gate activates only for **deploy** and **update** actions. Restart, stop, and scan do not trigger it.
The health gate activates only for **Deploy** and **Update** actions; no other action triggers it.
<Frame>
<img src="/images/deploy-progress/modal-health-gate.png" alt="Deploy progress modal showing the health gate observing banner with an elapsed counter, the green health gate passed banner below it, and the full structured log body." />
<img src="/images/deploy-progress/modal-health-gate.png" alt="Deploy progress modal showing the health gate observing banner with an elapsed counter above a fully populated structured log body." />
</Frame>
## Stage badges
@@ -77,12 +77,12 @@ Each log row is classified by content. Most lines render as **LOG** because the
Click **Raw output** in the footer to expand a raw terminal panel beneath the structured rows. The raw view shows the unprocessed compose stream, including the progress bars and ANSI-formatted output that the structured parser does not render. Clicking the toggle a second time (now labelled **Hide raw**) collapses the panel.
<Frame>
<img src="/images/deploy-progress/modal-raw-output.png" alt="Deploy progress modal with the Raw output panel expanded, showing structured rows above and the green-on-black raw terminal stream below." />
<img src="/images/deploy-progress/modal-raw-output.png" alt="Deploy progress modal with the Raw output panel expanded, showing structured rows and the health gate passed banner above the monospace raw terminal stream below." />
</Frame>
## Auto-close on success
When an action completes successfully, the status indicator switches to `Succeeded · closes in <n>s` and a 4-second countdown begins. Hover anywhere over the modal to pause the countdown; moving the cursor away restarts a fresh 4 seconds. Clicking **Close** dismisses the modal immediately.
When an action completes successfully, the status indicator switches to a green `Succeeded` label with a `closes in <n>s` countdown next to it, and a 4-second countdown begins. Hover anywhere over the modal to pause the countdown; moving the cursor away restarts a fresh 4 seconds. Clicking **Close** dismisses the modal immediately.
When the [health gate](#health-gate) is observing the stack after a deploy or update, the modal withholds the success verdict: the status indicator shows **Verifying health** instead of **Succeeded**, and the auto-close waits for the gate result. A passed gate resumes the countdown; a failed or unknown result keeps the modal open until you close it manually.
@@ -108,10 +108,10 @@ Click **Minimize** to collapse the modal to a small status pill anchored at the
The pill is mounted as a portal, so it persists across navigation: leave the App Store mid-install and the pill follows you to the dashboard, the editor, or any other view.
Minimizing, closing, or navigating away never cancels the operation. The progress view only displays output; the deploy, update, or stop runs to completion on its own. If you dismiss the view while live output is flowing, the run still finishes in the background, and its success or failure lands in your notifications.
Minimizing, closing, or navigating away never cancels the operation. The progress view only displays output; the triggering action runs to completion on its own. If you dismiss the view while live output is flowing, the run still finishes in the background, and its success or failure lands in your notifications.
<Frame>
<img src="/images/deploy-progress/pill.png" alt="Minimized deploy progress pill anchored at the bottom center of the dashboard, showing the brand-colored pulsing dot and the text 'Updating dozzle'." />
<img src="/images/deploy-progress/pill.png" alt="Minimized deploy progress pill, showing the pulsing status dot and the action verb followed by the stack name in monospace." />
</Frame>
## Inline style
@@ -135,32 +135,32 @@ When you navigate away from the stack detail while an operation is running, the
## Recovery actions
When a deploy or update fails, times out, or its outcome is ambiguous, the stack page offers safe next steps so you can fix the stack in place. On desktop a small **Update failed** chip appears in the stack card; click it to open a menu of recovery actions. On a phone the same actions show as an inline card on the stack detail. Either way, it shows the failed action, the error, and how long the operation ran, and it works whether or not the progress modal is enabled.
When a **deploy**, **update**, **restart**, or **rollback** fails, times out, or its outcome is ambiguous, the stack page offers safe next steps so you can fix the stack in place. On desktop a small chip naming the failed action (for example **Deploy failed** or **Update failed**) appears in the stack card; click it to open a menu of recovery actions. On a phone the same actions show as an inline card on the stack detail. Either way, it shows the failed action, the error, and how long the operation ran, and it works whether or not the progress modal is enabled.
The available actions are:
- **Retry** the failed operation.
- **Restart** the stack.
- **Roll back** to the previous version, shown only when a backup exists for the stack.
- **Restart** the stack, shown for any failure other than a failed restart itself.
- **Roll back** to the previous version, shown only when a backup exists and the failure wasn't itself a rollback.
- **Refresh** to re-read the live container status after the failure.
- **Copy details** to put the stack name, node, action, error, elapsed time, and last output line on your clipboard for a bug report.
Retry, Restart, and Roll back require deploy permission on the stack. After a failed or stalled operation, Sencho refreshes the container state automatically so the page reflects reality, and it never leaves the action stuck in an endless spinner.
Retry, Restart, and Roll back require deploy permission on the stack. **Stop**, **Take down**, **Install**, and **Scan this node** failures do not produce a recovery entry. After a failed or stalled operation, Sencho refreshes the container state automatically so the page reflects reality, and it never leaves the action stuck in an endless spinner.
## Supported entry points
The progress view opens for the following actions:
- **Deploy**, **Update**, **Restart**, **Stop** from the stack editor's action bar.
- **Deploy**, **Update**, **Restart**, **Stop**, **Take down** from the stack editor's action bar, sidebar context menu, or confirmation dialogs.
- **Install** from the App Store.
- **Apply** from a Git Source panel when the apply request includes a deploy.
- **Scanning** a stack's image configuration.
- **Scan this node** from the Security page's Overview tab, covering the vulnerability, secret, and compose-misconfiguration scan types you select. See [Vulnerability Scanning](/features/vulnerability-scanning#scanning-a-whole-node-at-once) for what each scan type checks.
Of those, **Deploy**, **Update**, **Install**, and Git **Apply** route through `docker compose up` and produce a populated structured-log body. **Restart** and **Stop** call the Docker Engine directly to act on existing containers; they bypass compose, so the modal opens, registers `0 lines`, and finishes. **Scanning** streams its own output as LOG rows.
Of those, **Deploy**, **Update**, **Install**, **Take down**, and Git **Apply** route through `docker compose up` or `docker compose down` and produce a populated structured-log body. **Restart** and **Stop** call the Docker Engine directly to act on existing containers; they bypass compose, so the modal opens, registers `0 lines`, and finishes. **Scan this node** streams its own sanitized progress lines (counts and rule IDs, never raw secret values) as LOG rows.
The [health gate](#health-gate) activates only after **Deploy** and **Update**; it does not fire for Restart, Stop, Install, Git Apply, or Scanning.
The [health gate](#health-gate) activates only after **Deploy** and **Update**; it does not fire for Restart, Stop, Take down, Install, Git Apply, or Scan this node.
The HTTP API exposes **Take down** as `POST /api/stacks/:name/down`. The stack header, sidebar context menu, and confirmation dialog call this endpoint. Compose output streams through the same progress modal as Deploy and Update. Optional `?removeVolumes=true` removes compose volumes when the node advertises support.
**Take down** is exposed over the HTTP API as `POST /api/stacks/:name/down`, with an optional `?removeVolumes=true` to remove compose volumes when the node advertises support.
## Troubleshooting
@@ -186,8 +186,8 @@ The HTTP API exposes **Take down** as `POST /api/stacks/:name/down`. The stack h
<Accordion title="The health gate is blocking the modal from closing">
The modal withholds auto-close while the health gate is observing containers and whenever the gate result is failed or unknown. A failed or unknown gate means the containers did not stay healthy through the observation window; the modal stays open so you can see the reason. Use the recovery actions on the stack page to retry, restart, or roll back. Clicking **Close** on the modal dismisses it manually at any time; it does not cancel the gate observation if it is still running.
</Accordion>
<Accordion title="No health gate appeared after a restart or scan">
The health gate only fires after **deploy** and **update** actions. Restart, stop, install, git apply, and scan complete without a post-operation observation period. If you want health monitoring after a restart, deploy or update the stack instead.
<Accordion title="No health gate appeared after a restart or a node scan">
The health gate only fires after **deploy** and **update** actions. Restart, stop, take down, install, Git apply, and Scan this node complete without a post-operation observation period. If you want health monitoring after a restart, deploy or update the stack instead.
</Accordion>
<Accordion title="I changed the deploy progress setting but it did not take effect">
The settings live in `localStorage`: the on/off toggle under `sencho.deploy-feedback.enabled` and the Modal/Inline choice under `sencho.deploy-feedback.style`. Both apply to the current tab without a reload, and other tabs in the same browser pick up the change through a `storage` event. Deploy progress defaults to on in Modal style, so only an explicit choice changes it. If a tab still does not honour the setting, refresh that tab. The settings do not sync across browsers or devices; each one carries its own choice.
+118 -19
View File
@@ -1,10 +1,10 @@
---
title: "Docker Label Audit"
sidebarTitle: Label audit
description: "Audit Docker and Compose labels that drive external automation across the fleet and inside each stack."
description: "Audit Docker and Compose labels that drive external automation across your fleet and inside each stack, with source provenance and Compose-vs-runtime reconciliation."
---
Docker labels are metadata declared on Compose services or attached to running containers. They are different from Sencho Stack Labels used for organizing stacks and Node Labels used for Blueprint placement.
Docker labels are metadata declared on Compose services or attached to running containers. They are different from [Stack Labels](/features/stack-labels) used for organizing stacks in the sidebar and Node Labels used for Blueprint placement.
Sencho surfaces two read-only audit views for these labels:
@@ -13,7 +13,14 @@ Sencho surfaces two read-only audit views for these labels:
## Fleet Docker Labels tab
Open **Fleet** and select the **Docker Labels** tab (after **Map**). The panel is titled **Docker label audit** and offers two layouts:
Open **Fleet** and select the **Docker Labels** tab, which appears after **Map**. The panel is titled **Docker label audit** and offers two layouts:
<Frame>
<img
src="/images/docker-label-audit/fleet-by-container.png"
alt="Fleet Docker Labels tab in the By container layout, listing every container across four nodes with its stack, node, state, and label count, plus an Open stack link"
/>
</Frame>
| View | What it shows |
|------|----------------|
@@ -22,35 +29,127 @@ Open **Fleet** and select the **Docker Labels** tab (after **Map**). The panel i
Use the search box to filter by label key, value, container name, stack, or node. When a container belongs to a known stack, **Open stack** jumps to that stack in the editor.
### Label provenance
Every label carries a provenance badge so you can tell where it came from:
- **Image** for labels inherited from the container image (for example OCI `org.opencontainers.image.*` metadata)
- **Present at runtime** for other labels set on the running container
- **Docker Compose system label** for keys starting with `com.docker.compose.`
- **Unknown** when a container or its image could not be inspected
| Badge | Meaning |
|-------|---------|
| **Image** | Inherited from the container image (for example OCI `org.opencontainers.image.*` metadata) |
| **Present at runtime** | Set on the running container by something other than the image or Compose's own bookkeeping |
| **Docker Compose system label** | A key starting with `com.docker.compose.` (project name, service, config hash, and similar bookkeeping Compose attaches to every container it creates) |
| **Unknown** | The container or its image could not be inspected, so provenance cannot be determined |
The fleet view reads container-level metadata and does not open the compose file, so it cannot tell a Compose-declared label from any other label set on the container: labels you declared in Compose appear here as **Present at runtime**. To see which labels come from the compose file, use the per-stack **Compose Labels** tab below. When a container or its image cannot be inspected, the affected labels show as **Unknown** and the panel names the nodes it could not fully inspect.
<Frame>
<img
src="/images/docker-label-audit/fleet-container-expanded.png"
alt="An expanded container row in the By container layout showing individual label rows, each with its key, value, and a provenance badge such as Image or Docker Compose system label"
/>
</Frame>
In the **By label** layout, open the **Filters** popover next to the search box. The **Defined by** section lists toggle pills for each provenance type present in the data (**Image**, **Runtime**, **System**, and **Unknown** when present). Turn a pill off to hide that source; the button shows a count badge while any filter is active, and **Clear filters** resets the popover. Automation tools such as Watchtower, Diun, and Traefik read these labels, so the audit makes it easy to confirm which containers are opted in or out.
The Fleet view reads container-level metadata only; it never opens the compose file, so it cannot tell a Compose-declared label from any other label set on the container. A label you declared under a service's `labels:` block in Compose appears here as **Present at runtime**, indistinguishable from a label some other process attached to the container. To see which labels genuinely come from the compose file, use the per-stack [Compose Labels tab](#stack-compose-labels-tab) below, which renders the compose model and can make that distinction.
### Filtering by source
In the **By label** layout, open the **Filters** popover next to the search box. The **Defined by** section lists a toggle pill for each provenance type actually present in the current data (only the sources your fleet's labels use appear, so a fleet with no unattributable labels never shows an **Unknown** pill). Turn a pill off to hide that source; the Filters button shows a count badge while any pill is off, and **Clear filters** resets the popover.
<Frame>
<img
src="/images/docker-label-audit/fleet-by-label-filters.png"
alt="The By label layout with the Filters popover open, showing a Defined by section with Image and System toggle pills, above a list of key=value rows each tagged with a container chip and provenance badge"
/>
</Frame>
Automation tools such as Watchtower, Diun, and Traefik read Docker labels to decide which containers to act on. Grouping **By label** makes it easy to confirm, fleet-wide, which containers carry a given automation label and which do not.
### Partial or unreachable nodes
A banner appears above the table whenever the fleet-wide fetch was incomplete:
- **Could not reach *node*.** The node did not respond to the label-inventory request, whether because it is offline, the network call timed out, or (on a mixed-version fleet) the node does not implement the endpoint at all. That node contributes no rows to either layout.
- **Some containers or images could not be inspected on *node*.** The node responded, but `docker inspect` failed for one or more containers or their images on that node. The affected labels show the **Unknown** provenance badge instead of being silently dropped.
Both conditions can appear together across different nodes in the same fleet. The rest of the data from healthy nodes is still shown; the banner only names what is missing.
Runtime labels are static until the container is recreated. Changes declared in Compose require save and redeploy before they appear on running containers.
## Stack Compose Labels tab
Inside the stack editor, open the **Compose Labels** tab in the anatomy strip. For each service you see:
Inside the stack editor, open the **Compose Labels** tab in the anatomy strip.
- **Declared in Compose** labels from the effective rendered compose model
- **Present at runtime** labels read from each running replica, each with its provenance badge (Compose, Image, or runtime)
- Reconciliation hints: **only in Compose**, **only on running container**, **present in both**, or **value changed** when a key is declared and running but the values differ
<Frame>
<img
src="/images/docker-label-audit/stack-compose-labels.png"
alt="Stack Compose Labels tab showing a service card with a badge reading '24 only on running container', followed by label rows each showing a key, value, and a Docker Compose system label or Image provenance badge"
/>
</Frame>
Because this tab renders the compose model, it can identify Compose-declared labels accurately. A toolbar at the top combines a **search box** with a **Filters** popover. The search matches label keys and values as well as service and container names. The popover has a **Defined by** section with toggle pills for each provenance present (**Compose File**, **Image**, **Runtime**, **System**) and a **Services** section to show or hide individual service cards when the stack has more than one service. The Filters button shows a count badge for active facet and service filters; **Clear filters** resets both sections. Matching a service or container name reveals that parent's labels even when the text does not match a specific key or value. Reconciliation counts always reflect only the labels currently visible.
Because this tab renders the effective compose model instead of only reading container metadata, it can tell a Compose-declared label from a runtime one with confidence. For each service you see:
When Compose cannot be fully rendered, the panel warns that declared labels may be incomplete but still shows whatever runtime data is available. If a replica cannot be inspected, it is flagged with **Runtime labels unavailable** and the panel notes that provenance may be incomplete.
- **Declared in Compose** labels, read from the effective rendered compose model
- **Present at runtime** labels, read from each running replica, each with its own provenance badge (Compose, Image, Runtime, or System)
- Reconciliation hints per replica: **only in Compose** (declared but not present on the container), **only on running container** (present but not declared, including Compose's own system labels and any image labels), **present in both**, or **value changed** when a key is declared and running but the values differ
## Sensitive values
A toolbar combines a **search box** with a **Filters** popover. The search matches label keys and values as well as service and container names; matching a service or container name reveals that parent's labels even when the text does not match a specific key or value. The popover has a **Defined by** section with a toggle pill for each provenance present (**Compose File**, **Image**, **Runtime**, **System**) and, only when the stack has more than one service, a **Services** section to show or hide individual service cards. The Filters button shows a count badge for active facet and service filters; **Clear filters** resets both sections. Reconciliation counts always reflect only the labels currently visible, so filtering down to one provenance also narrows the badge counts to match.
Some label keys look like secrets (for example keys containing `token`, `password`, or `auth`). Their values are redacted by default. Admins can reveal full values with the **Reveal** control, which re-fetches the inventory with elevated read access.
When Compose cannot be fully rendered, the panel warns that declared labels may be incomplete. Concretely: the **Declared in Compose** section shows nothing for that stack, reconciliation hints are skipped entirely, and every runtime label other than Compose's own system labels falls back to **Unknown** provenance rather than a guessed value. Runtime data itself is still shown. If a specific replica cannot be inspected, that replica is flagged **Runtime labels unavailable** instead, while the rest of the stack's services and replicas render normally.
## Editing labels
## Sensitivity and redaction
Compose label editing from these panels is not available yet. To change labels today, edit the compose file directly and redeploy the stack.
A label key that looks like it names a secret is redacted by default. The classification is a heuristic based on the label's key name, not its value; the same logic used by the [Environment Guardrails](/features/environment-guardrails) inventory applies here, so a key containing a whole-word segment like `token`, `password`, `secret`, `key`, `credential`, or `auth` is flagged. Two Docker/Compose-specific patterns are added on top: Traefik's `basicauth` and `digestauth` middleware label keys are always treated as secret, since their values embed inline credentials regardless of what the rest of the key is named. Like the environment classifier, it errs toward marking things as secret: a false positive only hides a value the audit never needed to show.
A redacted value displays a lock icon in place of the real text. Admins can reveal full values with the **Reveal** control next to a redacted label, which re-fetches the inventory with elevated read access for that view. Reveal has no corresponding hide action: once revealed, the view stays revealed until you navigate away or reload, and each of the Fleet and Stack views tracks this independently, so revealing values in one does not reveal them in the other.
## Capability gating
Both tabs require the active node to report the `container-label-inventory` capability; a node running an older Sencho release does not advertise it, and the tab is hidden entirely until the node is updated. There is no tier gate: Docker Label Audit is available on every plan.
For the **Fleet** tab specifically, the gate is evaluated against the active (normally local) node, not every enrolled node individually. A remote fleet node that lacks the capability does not hide the whole tab; instead, that node's containers fail to fetch and it shows up in the ["Could not reach" banner](#partial-or-unreachable-nodes) alongside any other unreachable node.
## Limitations
- **Read-only.** Neither view can add, edit, or remove a label. To change a label, edit the compose file directly and redeploy the stack; see [Editing the stack](/features/editor).
- **Live, not stored.** Every time you open a tab, switch layouts, or press Refresh, Sencho re-fetches current state from Docker. There is no history and no point-in-time snapshot to compare against.
- **Fleet cannot attribute Compose provenance.** The Fleet tab only ever sees container-level metadata, so a Compose-declared label and an unrelated runtime label are indistinguishable there. Use the Stack Compose Labels tab when you need to know for certain whether a label came from Compose.
- **Provenance depends on successful inspection.** A container or image that Docker cannot inspect (permissions, a removed image, a slow or unresponsive daemon) yields **Unknown** provenance for its otherwise-unattributed labels rather than a guess.
- **Runtime labels are a snapshot of the running container.** A label change in Compose has no effect on what is shown here until the affected container is recreated.
## Troubleshooting
<AccordionGroup>
<Accordion title="A label I declared in Compose shows as Present at runtime in the Fleet view">
This is expected. The Fleet Docker Labels tab reads only container-level metadata and has no access to the compose file, so it cannot confirm a label originated in Compose; anything it cannot attribute to the image or to Compose's own system labels is shown as **Present at runtime**. Open the stack's **Compose Labels** tab instead, which renders the compose model and correctly shows the same label under **Declared in Compose**.
</Accordion>
<Accordion title="Some labels show Unknown provenance">
Provenance for a label falls back to **Unknown** when Sencho could not inspect the container or its image on that node. Confirm the node's Docker daemon is reachable and that the image referenced by the container still exists locally, then press **Refresh**.
</Accordion>
<Accordion title="The Docker Labels or Compose Labels tab is not visible">
Both tabs require the active node to report the `container-label-inventory` capability. A node running an older version of Sencho does not advertise it, so the tab is hidden for that node until it is updated. See [Capability gating](#capability-gating).
</Accordion>
<Accordion title="I don't see a Reveal button on a redacted value">
Revealing secret-like label values requires an admin role. If you are signed in as an admin and still do not see **Reveal**, the label may not be classified as redacted (or already has been revealed for this view). See [Sensitivity and redaction](#sensitivity-and-redaction).
</Accordion>
<Accordion title="The Compose Labels tab warns it could not fully render Compose">
This means `docker compose config` failed for the stack, the same render step [Compose Doctor](/features/compose-doctor) depends on. While unresolved, the **Declared in Compose** section is empty and reconciliation hints do not appear; runtime data is still shown, with **Unknown** provenance. Fix the underlying Compose or environment problem (check Compose Doctor's report on the same stack) and reopen the tab.
</Accordion>
<Accordion title="A container shows 'Runtime labels unavailable'">
Sencho could not inspect that specific replica, so its runtime labels and reconciliation hints are skipped rather than shown as false gaps. The rest of the stack's services and replicas are unaffected. Confirm the container still exists and the node's Docker daemon is reachable, then reopen the tab.
</Accordion>
</AccordionGroup>
## Related features
<CardGroup cols={2}>
<Card title="Stack Labels" icon="tag" href="/features/stack-labels">
A different, editable kind of label: per-node tags used to group stacks in the sidebar, unrelated to Docker/Compose labels.
</Card>
<Card title="Compose Doctor" icon="stethoscope" href="/features/compose-doctor">
Renders the same effective Compose model to check for deploy-blocking problems, separate from label reconciliation.
</Card>
<Card title="Environment Guardrails" icon="key" href="/features/environment-guardrails">
Uses the same key-name heuristic to flag likely-secret environment variables without ever exposing a value.
</Card>
<Card title="Node Compatibility" icon="puzzle-piece" href="/features/node-compatibility">
How capability gating works across a fleet with mixed Sencho versions.
</Card>
</CardGroup>
+22 -2
View File
@@ -27,6 +27,10 @@ The top card on the left holds the stack's identity and primary controls.
The action bar runs every state transition for the whole stack. The primary buttons (**Start**, **Restart**, **Stop**, **Take down** when running, **Update**) require the `stack:deploy` permission; the **Delete** entry in the kebab dropdown requires the `stack:delete` permission. The bar still appears when only **Delete** is authorised so the operator has a way to remove the stack.
<Note>
If a stack is the Sencho instance you are currently signed into, its deploy/delete actions are protected: clicking any of them opens a **Sencho instance protected** dialog instead of running the action. Update Sencho from **Fleet → Node Update**, or move Sencho's own compose project outside `COMPOSE_DIR` to manage it as a normal stack.
</Note>
| Button | Behavior |
|--------|----------|
| **Start** / **Restart** | A single button that becomes **Restart** when at least one container is running and **Start** otherwise. |
@@ -63,6 +67,13 @@ Below the action bar, the **CONTAINERS** section lists every container the stack
When the container has a compose service name attached, an extra `⋮` button appears at the right of the row. It opens **Restart service**, **Stop service**, or **Start service** depending on the current state. Service actions affect a single service; for full-stack operations use the action bar above.
### Multi-container stacks
Stacks with more than one container gain a summary strip above the container list: a total count plus `up`, `paused`, and `unhealthy` counts (only the non-zero ones show). Two view controls sit beside it:
- **Compact / Detailed** toggles whether the CPU/Mem/Net sparkline grid renders under every container row. Detailed is the default for a single-container stack; multi-container stacks default to Compact.
- **Expand containers** collapses the Logs pane so the container list fills the whole left column. Expanding containers and expanding logs (below) are mutually exclusive; opening one closes the other.
## Anatomy panel
The right column shows the **Anatomy panel** by default: a read-only summary of the compose file alongside a scrollable tab row for other stack views.
@@ -99,6 +110,12 @@ The Anatomy tab lists:
When an image update is available, or the stack declares services with a local `build:` section, an inline banner appears at the top of the panel. Registry updates follow version-bump severity: `safe to apply` (patch), `review recommended` (minor), `breaking changes possible` (major), or `review required` when the bump cannot be classified. Build-only stacks show **Rebuild available** with a **Rebuild & Update** button. The banner runs the same operation as the action bar's **Update**; it is hidden for roles that lack the `stack:edit` permission and when the bump is flagged as blocked.
If the most recent post-deploy vulnerability scan for this stack failed, partially failed, or was skipped, a dismissible amber banner appears below the update banner (`Last post-deploy scan failed.`, `...partially failed.`, or `Post-deploy scan did not run.`). See [Vulnerability Scanning](/features/vulnerability-scanning) for what triggers a post-deploy scan.
<Frame>
<img src="/images/editor/scan-status-banner.png" alt="Anatomy panel showing a post-deploy scan failed banner below the compose facts" />
</Frame>
## Editor mode
Clicking **Edit compose** in the Anatomy strip opens an editable Monaco workspace. Creating a new empty stack does the same automatically.
@@ -113,7 +130,7 @@ The editor card has three tabs:
|-----|---------|
| `compose.yaml` | YAML editor with syntax highlighting. Always available. |
| `.env` | Environment file editor with syntax highlighting. Disabled when the stack has no env file. |
| **Files** | Browseable directory tree for the stack folder. See the dedicated [Stack File Explorer](/features/stack-file-explorer) page for permissions, upload caps, and protected-file behavior. |
| **Files** | Browseable directory tree for the stack folder. See the dedicated [Stack File Explorer](/features/stack-file-explorer) page for permissions, upload caps, and protected-file behavior. A full-screen toggle appears next to **Close editor** while this tab is active, collapsing the left column so the file tree and editor use the full width. |
Above the editor area, the toolbar carries:
@@ -173,7 +190,7 @@ Below the Command Center the left column reserves the rest of its height for the
| **Structured** | Parses each line for an ISO 8601 timestamp and severity (info, warn, err) and renders rows with filter chips (`all`, `info`, `warn`, `err`) and a counter on the active level. Strips ANSI control codes and auto-follows new lines while you stay at the bottom; scrolling up reveals a **resume follow** button. The buffer is capped at 10,000 lines and the **Download logs** button writes the current buffer to a `.txt` file. |
| **Raw terminal** | A full xterm.js terminal that streams the combined container output verbatim with ANSI color preserved. Press `Ctrl+F` to open the inline find bar. The scrollback is 10,000 lines and a download button is available in the toolbar. |
Both modes connect to the same WebSocket and switch instantly without losing the stream.
Both modes connect to the same WebSocket and switch instantly without losing the stream. In Structured mode, an **Expand logs** button next to **Download logs** collapses the Command Center card so the log pane fills the left column; click again (now **Collapse logs**) to bring the Command Center back. This control is mutually exclusive with the container-list **Expand containers** toggle above.
## Log viewer
@@ -234,6 +251,9 @@ Sencho tries `/bin/bash` first and transparently falls back to `/bin/sh` if bash
<Accordion title="Switching nodes prompts me to discard unsaved changes">
The editor blocks a silent loss of in-progress edits. Click **Cancel** to return to the original node with your edits intact; click **Discard** to abandon them and proceed to the other node.
</Accordion>
<Accordion title='"Sencho instance protected" dialog when I try to stop, update, or delete a stack'>
That stack is running the Sencho instance you are signed into, and its deploy/delete actions are blocked to prevent locking yourself out. Update Sencho from **Fleet → Node Update** instead. To manage the stack normally, move its compose project to a directory outside `COMPOSE_DIR`.
</Accordion>
<Accordion title='"Stats unavailable" chip on the container list'>
The stats WebSocket failed to open, or closed unexpectedly. This usually means the Docker daemon on the node is unreachable. Container status, action buttons, and logs continue to work; only live CPU / memory / network rates pause.
</Accordion>
+3 -3
View File
@@ -9,7 +9,7 @@ The **Environment** tab in the right-hand **Anatomy** panel answers a question C
The inventory is advisory and read only. It never changes a stack, and it works entirely from variable **names**: a value is never read into the report, the checklist, or the logs, so nothing sensitive is exposed.
<Frame>
<img src="/images/environment-guardrails/env-overview.png" alt="Environment tab open on a stack. The panel shows a PROJECT ENVIRONMENT FILE section, a summary line reading '19 vars · 2 likely secret', an ENV FILES section with .env marked missing and a globals.env marked unverifiable, and a PRESENT group listing injected variables." />
<img src="/images/environment-guardrails/env-overview.png" alt="Environment tab open on a stack. The panel shows a PROJECT ENVIRONMENT FILE section reading 'Using .env (default)', a summary line reading '18 vars · 2 likely secret', an ENV FILES section with .env marked missing and a second file marked unverifiable, and a PRESENT group listing injected variables." />
</Frame>
## Interpolation versus container injection
@@ -55,7 +55,7 @@ The classification is a heuristic based on the variable name. It errs toward mar
## Env file status
The **ENV FILES** section lists every env file the stack declares, with a three-state badge showing whether the file is reachable:
The **ENV FILES** section lists every `env_file:` entry the stack declares, plus the project environment file only when it has a problem (missing or unverifiable). A project file that resolves cleanly is already shown by name in the **Project environment file** panel above, so it is not repeated here. Each listed file carries a three-state badge showing whether it is reachable:
| Badge | Meaning |
|-------|---------|
@@ -92,7 +92,7 @@ A `${VAR:?message}` reference tells Compose the variable is required: the deploy
If you would rather fail fast with a clear message before anything runs, turn on **Block deploy on missing required env vars** under **Settings → Infrastructure → Stacks → Deploy Guardrails**. With it on, a deploy or update is refused up front when a required variable is unset or empty, before any backup, image pull, or container change happens. It is off by default, applies to the node you set it on, and requires an admin to change.
<Frame>
<img src="/images/environment-guardrails/settings-deploy-guardrails.png" alt="Settings page at Infrastructure > Stacks showing the Deploy Guardrails section. The 'Block deploy on missing required env vars' toggle is set to OFF." />
<img src="/images/environment-guardrails/settings-deploy-guardrails.png" alt="Settings page at Infrastructure > Stacks showing the Deploy Guardrails section, scoped to this node. The 'Block deploy on missing required env vars' toggle is set to OFF, alongside the related 'Observe health after updates' and 'Observation window' guardrails." />
</Frame>
## Opening the inventory
+136 -73
View File
@@ -3,16 +3,16 @@ title: "Fleet Actions"
description: "Bulk operations across the fleet from one tab: stop stacks by label, assign a label to stacks across nodes, and reclaim Docker disk space on every node."
---
The **Actions** tab on the Fleet view groups bulk operations that touch more than a single stack on a single node. Each action lives in its own card, runs from the control instance, and reports per-node and per-stack results inline so you never have to click through a modal to learn what happened.
The **Actions** tab on the Fleet view groups bulk operations that touch more than a single stack on a single node. Each action lives in its own card, orchestrates from the control instance, and reports per-node and per-stack results inline so you never have to click through a modal to learn what happened.
Three cards ship today: **Stop fleet by label**, **Bulk label assign**, and **Prune Docker resources fleet-wide**.
Three cards ship today: **Prune Docker resources fleet-wide**, **Bulk label assign**, and **Stop by label**. Every card follows the same discipline before it touches anything: a live, debounced readout resolves the exact blast radius as you type or select, and the destructive or state-changing button stays disabled until that readout resolves to a concrete node and stack list. You confirm against real names, not a label string or a byte estimate.
<Frame>
<img src="/images/fleet-actions/fleet-actions-overview.png" alt="Fleet view with the Fleet Actions tab selected. Three side-by-side cards: Stop fleet by label (rose rail), Bulk label assign (purple rail), and Prune Docker resources fleet-wide (amber rail). Each card carries its own form controls and a calm warning callout beneath the primary button." />
<img src="/images/fleet-actions/fleet-actions-overview.png" alt="Fleet view with the Actions tab selected. A two-column grid: Prune fleet-wide (top left, Maintenance chip), Bulk label assign (top right, Transformative chip), and Stop by label (bottom, Destructive chip). Each card shows a toolbar row with an action-class chip, a live blast-radius readout, a Dry run or Reset button, and the primary action." />
</Frame>
<Note>
Fleet Actions runs admin-only. Operator and viewer roles see the tab but cannot apply the cards.
Fleet Actions runs admin-only. Operator and viewer roles see the tab and every card, but cannot apply them.
</Note>
## What Fleet Actions covers (and what it doesn't)
@@ -26,77 +26,93 @@ Fleet Actions is the home for operations that span the fleet but don't fit anywh
| Trigger a Sencho self-update across remote nodes | **Check Updates** button on the Fleet masthead |
| Steer where new blueprint deployments land | [Fleet Federation](/features/fleet-federation) |
| Replicate scan policies and CVE suppressions to remotes | [Fleet Sync](/features/fleet-sync) |
| Reclaim disk space on a single node with an itemized, undo-safe preview | **Resources → Prune** on that node (a different endpoint and flow from the fleet-wide card; see [Prune Docker resources fleet-wide](#prune-docker-resources-fleet-wide)) |
## Three cards, three execution paths
## How every card works: preview, confirm, execute
The cards share a tab and a role gate, but they don't share an execution path. Knowing which path runs explains the result panels and the failure modes.
All three cards share one execution model, and understanding it explains every result panel, timeout, and edge case below.
| Card | Endpoint | Where it runs | Scope |
|---|---|---|---|
| Stop fleet by label | `POST /api/fleet/labels/fleet-stop` | Control instance orchestrates; fans out to each node | Every configured node |
| Bulk label assign | `POST /api/fleet/labels/bulk-assign` | Control instance orchestrates; fans out to each target node | The stacks you select across nodes |
| Prune Docker resources fleet-wide | `POST /api/fleet/labels/fleet-prune` | Control instance orchestrates; fans out to each node | Every reachable node |
1. **You describe the target.** A stack label name (Stop by label), a label plus checked stacks (Bulk label assign), or a set of resource targets and a scope (Prune).
2. **A live, debounced readout resolves the real blast radius.** Typing a label name or checking a target fires a non-destructive preview call (`POST /api/fleet/labels/match-preview` or `POST /api/fleet/prune/estimate`) roughly 350-500ms after you stop changing input. The readout in the card's toolbar shows `awaiting target` until something is selected, `resolving…` while the call is in flight, and then a concrete count (`7 stacks · 1 nodes`, `~ 6.09 GB reclaimable`). The primary button stays disabled until this resolves to a non-zero, non-loading result.
3. **You confirm against the resolved list, not the input.** Clicking the primary button opens a confirmation dialog that lists the actual nodes and stacks (Stop, Bulk assign) or restates the scope (Prune). For Stop by label specifically, the confirmation carries the exact node/stack list the preview resolved, and the real stop only touches stacks that are still in that list *and* still carry the label at execution time: a stack that gains the label after you opened the confirmation is never touched, and a node that reconnects after the preview does not get pulled into the stop.
4. **The control instance fans the confirmed action out to every node in parallel.** The local node runs in-process; each remote node is called over the standard Bearer-token proxy path. A node that cannot be reached, returns a non-2xx response, or returns a shape Sencho does not recognize is reported as a failure for that node only; the fan-out to every other node still completes.
5. **Results render per node, grouped and expandable**, in a `Per-node breakdown` section below the form. Stop and Prune also expose a **Dry run** button that walks the identical code path and locks without performing the destructive step, so you can rehearse the exact fan-out before committing.
All three cards orchestrate from the control instance and report results grouped by node. Stop and Prune iterate every node in **Settings → Nodes**; Bulk label assign iterates only the nodes whose stacks you selected. Each card runs the authoritative work on the executing node (the local node in process, every remote over the node proxy), so unreachable nodes show up in the results with a transport error rather than blocking the rest of the batch.
## The three cards
## Stop fleet by label
| Card | Class | Destructive endpoint | Preview / estimate endpoint | Scope |
|---|---|---|---|---|
| Stop by label | Destructive | `POST /api/fleet/labels/fleet-stop` | `POST /api/fleet/labels/match-preview`, `GET /api/fleet/labels/suggestions` | Every configured node |
| Bulk label assign | Transformative | `POST /api/fleet/labels/bulk-assign` | (computed client-side from `/api/labels` and `/api/fleet/node/:id/stacks` per node) | Only the nodes whose stacks you select |
| Prune Docker resources fleet-wide | Maintenance | `POST /api/fleet/labels/fleet-prune` | `POST /api/fleet/prune/estimate` | Every configured node |
Stop every stack assigned a given **stack label** on every node where that stack label exists. Stack labels are matched **by name** across the fleet, so a stack label called `production` on one node and an independently-authored `production` label on another node both match. This action targets stack labels only; node labels (used for node grouping) are never used here. See [Stack Labels](/features/stack-labels) for how to author the selector taxonomy.
Every card is admin-only and available on every license tier. Stop and Prune iterate every node in **Settings → Nodes**; Bulk label assign iterates only the nodes whose stacks you actually checked. Each card runs the authoritative work on the executing node (the local node in process, every remote over the node proxy), so an unreachable node shows up in the results with a transport error rather than blocking the rest of the batch.
## Stop by label
Stop every stack assigned a given **stack label** on every node where that stack label exists. Stack labels are matched **by name, case-sensitively,** across the fleet, so a stack label called `Media` on one node and an independently-authored `Media` label on another node both match, while `media` does not. This action targets stack labels only; node labels (used for node grouping) are never used here. See [Stack Labels](/features/stack-labels) for how to author the selector taxonomy.
### Step by step
1. Open **Fleet → Actions**.
2. Type a name in the **Stack label** field. The picker queries each reachable node for its own stack labels and lists each name once with its combined stack and node counts (and the carrying node names), so the scope is unmistakable. A stack label that exists only on a remote node still appears. When a node cannot be reached, the picker notes that the suggestions may be incomplete. You can also type a name by hand.
3. As you type, a live preview resolves the blast radius across the fleet and the card lists which nodes and stacks match. **Stop fleet** stays disabled until that preview resolves to at least one matching stack, so the action can never run before you can see what it will hit. If the live preview is unavailable, **Dry run** rehearses the same fan-out without stopping anything and resolves the blast radius the same way.
4. Click **Stop fleet**.
5. A confirmation appears with the kicker **Fleet stop**, the title `Stop all stacks with the stack label "<name>"?`, and the resolved list of nodes and stacks that will be stopped. Review it, then click **Stop fleet** to commit.
2. Type a name in the **Stack label** field. The autocomplete queries every reachable node for its own stack labels (`GET /api/fleet/labels/suggestions`) and lists each name once with its combined stack and node counts. A stack label that exists only on a remote node still appears. When a node cannot be reached, a note under the field warns that suggestions may be incomplete. You can also type a name that isn't suggested; the live preview and the eventual stop still resolve it authoritatively against every node.
3. As you type, the **Preview** section resolves the live blast radius: which nodes carry the label, how many stacks match on each, and which nodes could not be reached. The toolbar readout mirrors this (`7 stacks · 1 nodes`), and **Stop fleet** stays disabled until it resolves to at least one matching stack.
4. Click **Stop fleet**. A confirmation dialog opens with the kicker **Fleet stop**, the title `Stop all stacks with the stack label "<name>"?`, and the resolved node/stack list ("Will stop 7 stacks across 1 node. Only these stacks are stopped, and only while they still carry the label.").
<Frame>
<img src="/images/fleet-actions/fleet-actions-stop-confirm.png" alt="Fleet stop confirmation dialog. Kicker 'Fleet stop' in red mono, italic title 'Stop all stacks with the stack label &quot;docs-preview&quot;?', Cancel and Stop fleet buttons in the footer." />
<img src="/images/fleet-actions/fleet-actions-stop-confirm.png" alt="Fleet stop confirmation dialog. Kicker 'FLEET STOP' in red mono, title Stop all stacks with the stack label 'Media'?, a description of the effect, a bordered list reading 'LOCAL bazarr, plex, radarr, seerr, sonarr, tautulli, threadfin', and Cancel / Stop fleet buttons in the footer." />
</Frame>
5. Click **Stop fleet** again to commit. If the live preview is unavailable, running **Dry run** first resolves the same blast radius through the destructive endpoint's own dry-run path and unblocks the real stop against that snapshot instead.
### Reading the per-node breakdown
When the request finishes, the results render below the form, grouped by node. Each node row carries a colored icon and either a stack count or a `(no matching stack label)` annotation; the indented children below each row are the per-stack results.
When the request finishes, a **Per-node breakdown** section renders below the form with a `Results` badge pair (`N ok` / `N failed`). Each node row carries a status icon and either a stack count or a `(no matching stack label)` annotation; the indented children below each row are the per-stack results.
<Frame>
<img src="/images/fleet-actions/fleet-actions-stop-results.png" alt="Per-node breakdown after running fleet stop against a label that no node has. The header reads PER-NODE BREAKDOWN with two badges, '0 ok' and '7 failed'. Seven rows follow, one per node (Local, Opsix, Pitt-Moba, SLX-Mars, sencho-pilot-test, sencho-test-01, sencho-test-02), each annotated '(no matching stack label) · Stack label not present'." />
<img src="/images/fleet-actions/fleet-actions-stop-results.png" alt="Per-node breakdown after a dry run against the Media label. Results badges read '1 ok' and '3 failed'. Local shows 7 stacks (dry run) with all seven listed as succeeded. Opsix, Pitt-Moba, and SLX-Mars each show '(no matching stack label) · Stack label not present'." />
</Frame>
A few quirks worth knowing:
- A node that has no stack label by that name appears as `<node> (no matching stack label)` and is counted in the **failed** badge. This is not a transport failure, it just means the stack label was not present on that node.
- A node that has no stack label by that name appears as `<node> (no matching stack label)` and is counted in the **failed** badge. This is not a transport failure; it just means the label was not present on that node.
- A node where the label exists but no stacks are assigned to it appears with a matched count of zero stacks. No per-stack rows render.
- When the control instance reaches a remote node, the per-stack results you see come from the remote node's own response. A remote the control cannot reach, or that returns a non-2xx, is reported once as a single `<node> (unreachable)` row carrying the reason, not as a per-stack error, and it never blocks the stops on the reachable nodes.
- A remote the control cannot reach, or that returns a non-2xx or malformed response, is reported once as a single `<node> (unreachable)` row carrying the reason, not as a per-stack error, and it never blocks the stops on the reachable nodes.
### Confirmed-target binding and mixed-version fleets
The real stop (not the dry run) sends the confirmed node/stack list from the preview along with the label name. The backend intersects that list against the label's current, live assignment before stopping anything, so drift between the moment you saw the preview and the moment you clicked confirm cannot widen the blast radius in either direction: a stack that gained the label after the preview is never stopped, and a node that reconnects between preview and confirm is not pulled in.
This binding requires the remote node to understand the confirmed-target contract. If a remote is running an older Sencho version that does not advertise support for it, a real stop bound to specific stacks refuses to send to that node and reports it as needing an upgrade rather than silently falling back to stopping everything the label matches there. Dry runs carry no binding and are unaffected; they preview safely against any remote version.
### Behaviour and partial-failure semantics
- The endpoint always returns 200 with a `results` array. Partial failures live inside that array; the HTTP status is not the place to look.
- Each remote node call carries a 60-second timeout. A slow remote with many stacks can produce a clean per-stack list or a timeout row, depending on whether the remote streamed before the timeout fired.
- Every node, local or remote, runs the stop under a per-node bulk-action lock shared with the per-label action endpoint, so a fleet stop and a per-label stop aimed at the same node serialize cleanly instead of double-stopping the same containers. On a remote, the lock is held on that remote while it runs its share of the stop.
- The endpoint always returns `200` with a `results` array. Partial failures live inside that array; the HTTP status is not the place to look.
- Each remote node call carries a 60-second timeout. A slow remote with many stacks can produce a clean per-stack list or a timeout row, depending on whether the remote streamed a response before the timeout fired.
- Every node, local or remote, runs the stop under a per-node bulk-action lock shared with the per-label action endpoint (`POST /api/labels/:id/action`), so a fleet stop and a per-label stop aimed at the same node serialize cleanly instead of double-stopping the same containers.
## Bulk label assign
Add a stack label to stacks across the fleet in one round trip. Pick a label that exists anywhere in the fleet, select stacks on one or more nodes, and Sencho assigns that label on each target node, creating it there first if the node does not have it yet. Existing labels on the selected stacks are preserved. Use this to keep a label like `media` consistent across nodes without visiting each one to recreate and assign it by hand.
Add a stack label to stacks across the fleet in one round trip. Pick a label that exists anywhere in the fleet, check stacks on one or more nodes, and Sencho assigns that label on each target node, creating it there first if the node does not already have it. Existing labels on the selected stacks are preserved. Use this to keep a label like `Media` consistent across nodes without visiting each one to recreate and assign it by hand.
<Frame>
<img src="/images/fleet-actions/fleet-actions-bulk-assign.png" alt="Bulk label assign card. A label source row of pills with 'Media' selected, a target-stacks list grouped by node (Local and a remote) with several stacks checked, a preview showing 'create' on the remote and 'reuse' on Local, and a primary button reading 'Apply'." />
<img src="/images/fleet-actions/fleet-actions-preview-states.png" alt="Prune fleet-wide card (idle, Managed only scope, 0 reclaimable) beside Bulk label assign with the Media pill selected, all fifteen local stacks checked under Target stacks, and a Preview section reading 'REUSE Local · 15 stacks'." />
</Frame>
### Cross-node by label identity
Labels are node-local: each node owns its own copy of a label. Bulk label assign treats the label you pick as a name plus color, then resolves it per target node. If the node already has a label with that exact name, its own label is used; if not, Sencho creates one with the same name and color. The target node's own label is always used for the assignment, never the control node's.
Labels are node-local: each node owns its own copy of a label row. Bulk label assign treats the label you pick as a name plus color, then resolves it per target node. If the node already has a label with that exact name, its own label row is reused; if not, Sencho creates one with the same name and color. The target node's own label is always used for the assignment, never the control node's row.
### Step by step
1. Pick a label under **Label · source**. The pills list every stack label defined across the reachable fleet. If the same name carries different colors on different nodes, the local node's color is used where the label is created.
2. Under **Target stacks**, check the stacks you want, grouped by node. Use the filter to narrow long lists and **Select all** to take a whole node. Unreachable nodes are shown and cannot be selected.
3. The **Preview** shows, per node, whether the label will be **created** or **reused** and how many stacks it will touch.
4. Click **Apply**. A confirmation summarizes the blast radius; confirm to commit. Results render grouped by node, each row noting whether the label was created or reused alongside the per-stack outcome.
1. Pick a label under **Label · source**. The pills list every distinct stack label name defined across the reachable fleet. If the same name carries different colors on different nodes, the local node's color wins (or the most common color, if the local node doesn't have it); a note under the pills flags the conflict.
2. Under **Target stacks**, check the stacks you want, grouped by node. Use the filter field to narrow long lists and **Select all** / **Clear** to toggle a whole node at once. Unreachable nodes are shown but cannot be selected. **Refresh** re-reads stacks and labels from every node on demand; the list is loaded once when the card mounts and does not auto-poll.
3. The **Preview** section shows, per node with a selection, whether the label will be **created** or **reused** and how many stacks it will touch.
4. Click **Apply**. A confirmation dialog summarizes the blast radius and lists the affected stacks per node, flagging which nodes will create the label. Confirm to commit. Results render grouped by node, each row noting whether the label was created or reused alongside the per-stack outcome.
### Add, preserving existing labels
The selected label is **added** to each chosen stack; the stack keeps its other labels. The card only ever adds the one label you picked, so a fleet-wide propagation cannot accidentally wipe labels a remote node already carries.
The selected label is **added** to each chosen stack; the stack keeps every other label it already had. The card only ever adds the one label you picked, so a fleet-wide propagation cannot accidentally strip labels a remote node already carries. There is no replace or remove mode on this card.
### Batch ceiling
@@ -107,92 +123,123 @@ A single Apply accepts up to **1,000 stack assignments** summed across every tar
Reclaim disk space on every reachable node by deleting unused images, volumes, and networks. The control instance fans out to each node and reports reclaimed bytes per node and per target.
<Frame>
<img src="/images/fleet-actions/fleet-actions-prune.png" alt="Prune Docker resources fleet-wide card. Description reads 'Reclaim space on 7 nodes by removing unused images, volumes, and networks. Reclaimed bytes are approximate.' Below: TARGETS checkboxes (Images checked by default; Volumes; Networks). SCOPE segmented control (Managed only / All unused) with helper text 'Restricts to resources owned by stacks Sencho manages.' Primary button 'Prune across fleet'. A calm callout reads 'Prune is destructive and cannot be undone. Each target is run serially per node; reclaimed bytes appear per node and per target below.'" />
<img src="/images/fleet-actions/fleet-actions-prune.png" alt="Prune fleet-wide card with Images and Volumes targets checked, scope set to All unused, and a live per-node estimate: Local 153.85 MB, Opsix 3.06 GB, Pitt-Moba 1.37 GB, SLX-Mars 1.51 GB, totaling roughly 6.09 GB reclaimable in the toolbar readout." />
</Frame>
### Pick what to prune
The **TARGETS** checkboxes are independent and at least one must be ticked: **Images**, **Volumes**, **Networks**. The card defaults to Images alone, which is the cheapest and most common case.
The **Targets** checkboxes are independent and at least one must be ticked: **Images**, **Volumes**, **Networks**. The card defaults to Images alone, the cheapest and most common case.
### Managed only versus All unused
Scope is a segmented control with two options:
- **Managed only** (default). Sencho looks up the list of stacks it knows about on the node, then prunes only resources owned by those stacks. Active containers and resources that other tools placed on the host are untouched. Helper text reads *Restricts to resources owned by stacks Sencho manages.*
- **All unused**. Sencho runs `docker prune --all` for each selected target. Any image, volume, or network not currently in use is deleted, including resources from workloads Sencho does not manage. The confirmation title flips to **Prune ALL unused resources across the fleet?** and the confirm button reads **Prune everything unused**.
- **Managed only** (default). Sencho looks up the stacks it knows about on the node, then prunes only resources owned by those stacks. Active containers and resources placed by other tools are untouched.
- **All unused**. Sencho runs the equivalent of `docker system prune` for each selected target. Any image, volume, or network not currently in use is deleted, including resources from workloads Sencho does not manage. The confirmation title flips to **Prune ALL unused resources across the fleet?**.
### Behaviour and reclaimed bytes
### Live estimate and behaviour
- Each remote node receives one `POST /api/system/prune/system` per selected target, with a 120-second timeout. If a transport error fires for one target, the remaining targets on that node are short-circuited with the same error so a dead node does not absorb the full multi-target timeout budget.
- Local nodes serialize against a per-node lock (`bulk-prune:<nodeId>`). A second prune launched against the same local node while the first is still in flight returns *A prune is already running on this node* for each target.
- Reclaimed bytes are reported by the Docker daemon and are approximate. Per-node rows in the results panel sum the per-target reclaim; the per-target children show how much each prune actually freed.
- Changing a target or the scope re-triggers a debounced call to `POST /api/fleet/prune/estimate`, which walks the same Docker enumeration the destructive path uses so the estimate matches what pruning would actually reclaim. **Prune fleet** stays disabled until the estimate resolves: you cannot confirm a destructive fleet-wide prune with no context on what it will reclaim.
- Each remote node receives one `POST /api/system/prune/system` call per selected target, with a 120-second timeout. If a transport error fires for one target, the remaining targets on that node are short-circuited with the same error rather than retried, so a dead node doesn't absorb the full multi-target timeout budget.
- Local nodes serialize against a per-node lock (`bulk-prune:<nodeId>`). A second fleet prune launched against the same local node while the first is still in flight returns *A prune is already running on this node* for each target.
- Reclaimed bytes are reported by the Docker daemon and are approximate. Per-node rows in the results panel sum the per-target reclaim; the per-target children show how much each individual prune actually freed.
<Note>
Fleet Actions' prune card calls the same node-local prune route as the single-node **Resources → Prune** page, but without that page's itemized plan-and-fingerprint flow. It never returns the `PRUNE_PLAN_STALE` (409) error you can see on Resources; each fleet prune call targets exactly one resource type per node and executes immediately. If you want an itemized, reviewable plan before pruning a specific node, use that node's own Resources page instead.
</Note>
## Prerequisites
| Requirement | Why it matters |
|---|---|
| **Configured remote nodes in Settings → Nodes** | The two fan-out cards iterate the configured node list. A node missing its `api_url` or `api_token`, or one that cannot be reached, is reported once per node as unreachable (Stop fleet by label) or per target (Prune), and never blocks the reachable nodes. |
| **Labels you intend to target** | Stop fleet by label and the autocomplete depend on labels existing on at least one node. See [Stack Labels](/features/stack-labels) for the authoring flow. |
| **Configured remote nodes in Settings → Nodes** | Stop and Prune iterate the configured node list; Bulk label assign iterates whichever nodes you select stacks on. A node missing its `api_url` or `api_token`, or one that cannot be reached, is reported once per node as unreachable and never blocks the reachable nodes. |
| **Admin role** | Every card requires the admin role to apply. |
| **Labels you intend to target** | Stop by label and its autocomplete depend on stack labels existing on at least one node; Bulk label assign depends on at least one stack label existing anywhere in the fleet. See [Stack Labels](/features/stack-labels) for the authoring flow. |
## Behaviour and lifecycle
- **Always returns 200.** Every card is structured so that the HTTP status reflects the request shape, not the operational outcome. Partial failure is encoded in per-row fields, not in the status code.
- **Always returns 200.** Every destructive endpoint is structured so the HTTP status reflects the request shape, not the operational outcome. Partial failure is encoded in per-row fields, not in the status code.
- **No retry, no scheduling, no undo.** Fleet Actions runs synchronously and is operator-driven; there is no background scheduler and no roll-back. For recurrence, use [Scheduled Operations](/features/scheduled-operations).
- **Offline remotes still receive the request.** A node that is down at the moment of the action returns a transport-error row but does not block the fan-out across the rest of the fleet.
- **Concurrent runs serialize per node.** Both fan-out cards take per-node locks before hitting Docker, so kicking off a second prune (or a second fleet stop and a per-label stop) while the first is in flight yields a calm *A bulk action is already running on this node* / *A prune is already running on this node* row rather than silent overlap.
- **Concurrent runs serialize per node.** All three cards take per-node locks before touching Docker or the label tables, so kicking off a second prune, a second fleet stop, or a fleet stop overlapping a per-label stop on the same node yields a calm "already running on this node" row rather than silent double-execution.
## Limitations and non-goals
Fleet Actions is intentionally narrow in v1. The following are deliberately out of scope:
Fleet Actions is intentionally narrow. The following are deliberately out of scope:
- **No replace mode for bulk label assign.** The card only adds the label you pick; it never removes a stack's existing labels. To remove or swap labels on a stack, edit them from that stack's own view.
- **No fleet-wide bulk start, restart, or update.** Stop is the only fleet-wide stack lifecycle action today. Per-node multi-stack start, restart, and update live in the sidebar's Bulk mode.
- **No label-set selectors.** Stop fleet by label matches one label name. Combinations like "stacks labelled A AND B" are not supported.
- **No partial-stop ceiling.** The Stop card stops every stack the label matches; there is no "stop the first N" knob.
- **No label-set selectors.** Stop by label matches one label name. Combinations like "stacks labelled A AND B" are not supported.
- **No partial-stop ceiling.** The Stop card stops every stack the resolved blast radius contains; there is no "stop the first N" knob.
- **No undo.** A stopped stack stays stopped until you start it again; a pruned image is gone until it is pulled or rebuilt.
- **Approximate reclaim numbers.** The bytes the Prune card reports come from the Docker daemon and are best-effort, not authoritative.
- **60-second remote timeout on fleet-stop, 120-second remote timeout per prune target.** A remote with many stacks or a very slow filesystem may produce timeout rows before the work fully completes. The action itself usually still finishes on the remote; the control instance just stopped waiting.
- **Confirmed-target stops need a current remote.** A real (non-dry-run) stop bound to specific stacks refuses to run against a remote that doesn't advertise support for confirmed-target binding; upgrade the remote to retry.
- **Timeouts scale with the fan-out, not with any one node.** 60 seconds per remote on fleet-stop and bulk-assign, 120 seconds per remote per prune target. A remote with many stacks or a very slow filesystem may produce a timeout row before the underlying work fully completes; the action itself usually still finishes on the remote, the control instance just stopped waiting.
## Practical workflows
### Pause everything before a power event
Tag the stacks you want to bring down with a dedicated label (for example `evening-shutdown`). Run **Stop fleet by label** with that label name. The per-node breakdown confirms each stack's stop result; restart from the sidebar when power is back.
Tag the stacks you want to bring down with a dedicated label (for example `evening-shutdown`). Run **Stop by label** with that name and review the live preview before confirming. The per-node breakdown confirms each stack's stop result; restart from the sidebar when power is back.
### Rehearse a destructive action before committing
For Stop and Prune, click **Dry run** first. It walks the identical lock, fan-out, and per-node logic as the real action but skips the destructive leaf call, so the results panel shows exactly what would happen (including which nodes are unreachable right now) before you commit to it.
### Propagate a label across the fleet
Define a label like `media` on one node (for example the local node) under **Settings → Labels**, then open **Bulk label assign**, pick `media` under the label source, and check the matching stacks on every node. Sencho creates `media` on each node that does not have it and assigns it, leaving each stack's other labels intact. The label then matches fleet-wide in **Stop fleet by label** and in per-node stack filtering.
Define a label like `Media` on one node (for example the local node) under **Settings → Labels**, then open **Bulk label assign**, pick `Media` under the label source, and check the matching stacks on every node. Sencho creates `Media` on each node that does not have it and assigns it, leaving each stack's other labels intact. The label then matches fleet-wide in **Stop by label** and in per-node stack filtering.
### Free disk before a heavy deploy
Run **Prune Docker resources fleet-wide** with **Images** selected and **Managed only** scope. The reclaim shows up per node and gives you a quick read on which hosts had accumulated the most stale layers. Switch to **All unused** if you want the prune to reach workloads that Sencho does not manage.
Run **Prune Docker resources fleet-wide** with **Images** selected and **Managed only** scope, and check the live per-node estimate before confirming. It gives a quick read on which hosts have accumulated the most stale layers. Switch to **All unused** if you want the prune to reach workloads that Sencho does not manage.
## Common questions
<AccordionGroup>
<Accordion title="How is this different from Bulk mode in the sidebar?">
Bulk mode operates on a hand-picked set of stacks **on one node** and supports start, stop, restart, and update. Fleet Actions operates **across every configured node** by selector (a label, or a checked cross-node set), and only Stop is a lifecycle action here (Bulk mode covers restart and update, Fleet Actions does not).
</Accordion>
<Accordion title="Does Dry run touch anything?">
No. Dry run walks the same code path, including acquiring the per-node lock, but every card skips the destructive Docker or label-table call and returns what it would have done instead. It is safe to run repeatedly.
</Accordion>
<Accordion title="Why is the primary button disabled even though I typed a label or checked a target?">
Every destructive or state-changing button stays disabled until the live preview or estimate resolves to a non-zero, non-loading result. This is deliberate: you always confirm against a concrete, current blast radius rather than an unresolved input.
</Accordion>
<Accordion title="Why does Fleet's Prune never show the 'stale plan' error I've seen on Resources?">
Resources → Prune builds an itemized plan with a fingerprint and re-validates it at execute time, which is where that error comes from. Fleet Actions' prune card calls the simpler legacy single-target path on each node instead, so there is no plan to go stale.
</Accordion>
</AccordionGroup>
## Troubleshooting
<AccordionGroup>
<Accordion title="A node shows '(no matching stack label)' but I created the label there">
The fleet-stop match is by **stack label name**, not label ID. Confirm the stack label name on the affected node under **Settings → Labels**; a typo, a case mismatch, or a trailing space will leave the node out. Stack labels are scoped per node, so renaming the label on one node does not propagate to the others.
The fleet-stop match is by **stack label name, case-sensitively**, not label ID. Confirm the exact stack label name on the affected node under **Settings → Labels**; a typo, a case mismatch (`media` vs `Media`), or a trailing space will leave the node out. Stack labels are scoped per node, so renaming the label on one node does not propagate to the others.
</Accordion>
<Accordion title="The autocomplete didn't suggest a stack label I know exists">
The picker queries each reachable node for its own stack labels, so a name is missing when no reachable node has a **stack label** by that name, or when the node that owns it could not be reached when the picker loaded (the picker flags this case). Node labels never appear here, because this action targets stack labels only. You can always type a name by hand; the fleet-stop request still iterates every node and asks each one authoritatively.
The picker queries each reachable node for its own stack labels, so a name is missing when no reachable node has a **stack label** by that name, or when the node that owns it could not be reached when the picker loaded (a note under the field flags this case). Node labels never appear here, because this action targets stack labels only. You can always type a name by hand; the live preview and the fleet-stop request still resolve it authoritatively against every node.
</Accordion>
<Accordion title="Bulk label assign reports 'Invalid stack name' or 'Stack not found' for one row">
Stack names must match the standard validator: alphanumeric plus dash and underscore, no spaces, no path separators. A name that does not exist on its node returns *Stack not found*. Each stack is validated independently, so one bad row does not block the rest of the batch. Fix the offending entry and re-run; the rows that already succeeded are simply re-added with no change.
Stack names must match the standard validator: alphanumeric plus dash and underscore, no spaces, no path separators. A name that does not exist on its node returns *Stack not found*. Each stack is validated independently, so one bad row does not block the rest of the batch.
</Accordion>
<Accordion title="The Apply button on Bulk label assign is disabled">
Apply needs both a label picked under the source and at least one stack checked. An empty label source means no stack labels are defined on any reachable node yet; create one under **Settings → Labels** first. Wait for the per-node lists to finish loading before selecting.
</Accordion>
<Accordion title="Fleet stop timed out on one remote node">
Each remote node call carries a 60-second timeout. A remote with many stacks or a slow Docker daemon can outlast that budget. Check the affected remote's logs; the stop usually completed on the remote even though the control instance stopped waiting. Re-running the same fleet-stop is safe: stacks that are already stopped return the per-stack error *No containers found for this stack* and do not toggle anything else.
<Accordion title="A real Stop by label refuses to run against one node with an upgrade message">
That remote is running a Sencho version that doesn't advertise support for confirmed-target binding, the mechanism that binds the real stop to exactly the stacks you saw in the preview. Upgrade the remote node and retry; Dry run is unaffected and works against any version.
</Accordion>
<Accordion title="Fleet stop or bulk assign timed out on one remote node">
Each remote node call carries a 60-second timeout. A remote with many stacks or a slow Docker daemon can outlast that budget. Check the affected remote's logs; the action usually completed on the remote even though the control instance stopped waiting. Re-running is safe for Stop: stacks that are already stopped return the per-stack error *No containers found for this stack* and do not toggle anything else.
</Accordion>
<Accordion title="Prune across fleet reports 'A prune is already running on this node'">
The per-node prune lock is held while the first prune is in flight. Wait for the first run to finish or click **Refresh** on the Fleet masthead to confirm it has cleared, then re-run. The lock is released automatically when the first run exits.
The per-node prune lock is held while the first prune is in flight. Wait for the first run to finish, then re-run. The lock is released automatically when the first run exits.
</Accordion>
<Accordion title="The Apply button returns an error toast">
Fleet Actions runs admin-only. Confirm the active user has the admin role under **Settings → Users**. Operator and viewer roles see the cards but cannot apply them.
<Accordion title="A card's action returns a permission error even though I can see it">
Fleet Actions runs admin-only. Confirm the active user has the admin role under **Settings → Users**; operator and viewer roles see every card but cannot apply them.
</Accordion>
<Accordion title="A node is reported as unreachable (or shows a transport error per target)">
The node is in **Settings → Nodes** but its `api_url` or `api_token` is missing, expired, or unreachable. Stop fleet by label reports it once as a single `<node> (unreachable)` row; Prune reports it per target. Open **Settings → Nodes** on the control instance and click **Test connection** for the remote; fix the credential or the reachability, then re-run the action.
<Accordion title="A node is reported as unreachable">
The node is in **Settings → Nodes** but its `api_url` or `api_token` is missing, expired, or unreachable. Stop by label reports it once as a single `<node> (unreachable)` row; Prune reports it per target. Open **Settings → Nodes** on the control instance and test the connection for the remote; fix the credential or the reachability, then re-run the action.
</Accordion>
</AccordionGroup>
@@ -200,13 +247,29 @@ Run **Prune Docker resources fleet-wide** with **Images** selected and **Managed
Fleet Actions is one slice of the Fleet view. Each adjacent surface answers a different question.
| Related feature | What it covers | Why it is not Fleet Actions |
|---|---|---|
| [Stack Labels](/features/stack-labels) | Author the per-node label taxonomy that Fleet Actions targets. | Fleet Actions consumes labels; Stack Labels create and rename them. |
| [Stack Sidebar · Bulk mode](/features/sidebar#bulk-mode) | Per-node, per-stack bulk start, stop, restart, and update. | Bulk mode handles a hand-picked subset of stacks on one node; Fleet Actions runs by selector across many nodes. |
| [Scheduled Operations](/features/scheduled-operations) | Recurring or one-shot scheduled stack operations. | Scheduled Operations owns recurrence; Fleet Actions is always operator-initiated. |
| [Fleet Federation](/features/fleet-federation) | Operator-driven placement controls (cordon, pin) for [Blueprints](/features/blueprint-model). | Federation steers declarative placement; Fleet Actions runs imperative operations on what already exists. |
| [Fleet Sync](/features/fleet-sync) | Push-only replication of security rules from a control instance to its replicas. | Fleet Sync replicates state; Fleet Actions runs operations. |
| [Multi-Node Management](/features/multi-node) | How nodes get added to the fleet (proxy or pilot mode) and how the license tier propagates. | Multi-Node Management is the prerequisite; Fleet Actions runs against whatever Multi-Node Management already configured. |
| [Fleet View](/features/fleet-view) | The masthead, tab strip, and node grid that host the Actions tab. | Fleet Actions is one tab inside Fleet View. |
| [Licensing](/features/licensing) | Community and Admiral plans. | Licensing covers plan coverage across the product; Fleet Actions only cites the admin role check above. |
<CardGroup cols={2}>
<Card title="Stack Labels" icon="tag" href="/features/stack-labels">
Author the per-node label taxonomy that Fleet Actions targets. Fleet Actions consumes labels; Stack Labels create and rename them.
</Card>
<Card title="Stack Sidebar · Bulk mode" icon="list-check" href="/features/sidebar#bulk-mode">
Per-node, per-stack bulk start, stop, restart, and update. Handles a hand-picked subset of stacks on one node; Fleet Actions runs by selector across many nodes.
</Card>
<Card title="Scheduled Operations" icon="clock" href="/features/scheduled-operations">
Recurring or one-shot scheduled stack operations. Owns recurrence; Fleet Actions is always operator-initiated.
</Card>
<Card title="Fleet Federation" icon="sitemap" href="/features/fleet-federation">
Operator-driven placement controls for Blueprints. Steers declarative placement; Fleet Actions runs imperative operations on what already exists.
</Card>
<Card title="Fleet Sync" icon="arrows-rotate" href="/features/fleet-sync">
Push-only replication of security rules from a control instance to its replicas. Replicates state; Fleet Actions runs operations.
</Card>
<Card title="Multi-Node Management" icon="server" href="/features/multi-node">
How nodes get added to the fleet and how license tier propagates. The prerequisite; Fleet Actions runs against whatever Multi-Node Management already configured.
</Card>
<Card title="Fleet View" icon="table-cells" href="/features/fleet-view">
The masthead, tab strip, and node grid that host the Actions tab.
</Card>
<Card title="Licensing" icon="key" href="/features/licensing">
Community and Admiral plans. Fleet Actions is available on every tier; only the admin role check applies.
</Card>
</CardGroup>
+37 -13
View File
@@ -18,7 +18,7 @@ Create point-in-time snapshots of every `compose.yaml` and `.env` file across yo
4. Click **Create** to capture files from every reachable node
<Frame>
<img src="/images/fleet-backups/create-snapshot.png" alt="Inline create snapshot form with optional description field, showing Create and Cancel buttons above the snapshot table" />
<img src="/images/fleet-backups/create-snapshot.png" alt="Fleet Snapshots tab with the inline create form open above the table: a description textbox and Create and Cancel buttons" />
</Frame>
During creation, Sencho connects to each node in parallel:
@@ -29,7 +29,13 @@ If a remote node is offline or unreachable, it is **skipped gracefully**. The sn
### Scheduled snapshots
Fleet snapshots can also be created automatically on a recurring schedule. Navigate to the **Schedules** view, create a new scheduled operation, and select **Create Fleet Snapshot** as the action type. Scheduled snapshots appear in the snapshot list with a "Scheduled snapshot" prefix in their description.
Fleet snapshots can also be created automatically on a recurring schedule. Navigate to the **Schedules** view, create a new scheduled operation, and select **Create Fleet Snapshot** (grouped under the **Backups** category) as the action type. Scheduled snapshots appear in the snapshot list with a "Scheduled snapshot" prefix in their description.
### Triggered from an update
The stack update flow surfaces fleet snapshot coverage too. Before an admin updates or rebuilds a stack, the [Update Readiness](/features/health-gated-updates) dialog shows how long ago the most recent fleet snapshot covering that stack was taken, or that none exists yet, and offers a **Create a fleet snapshot before updating** checkbox. Checking it captures a fresh snapshot as part of proceeding; if the snapshot fails, the update does not start. These pre-update snapshots appear in the snapshot list with a "Pre-update snapshot: `<stack>`" description, alongside manual and scheduled ones.
The [Storage Portability](/features/compose-storage) tab surfaces the same coverage for any stack with persistent volumes: a warning card and a **Take a fleet snapshot →** link appear when the stack has no fleet snapshot in the last 7 days, or the relative time since the last one otherwise.
### Capturing stack documentation
@@ -47,8 +53,10 @@ The snapshot list shows each snapshot in a table with the following columns:
- **Warnings** - a warning icon with a count if any nodes or stacks were skipped, or "None"
- **Actions** - **View** to open the detail view, a cloud-upload icon for snapshots not yet mirrored to a configured Recovery Vault or custom off-site target, and a delete button for admins
The list paginates at 10 snapshots per page; use the arrows next to **Create Snapshot** to page through history.
<Frame>
<img src="/images/fleet-backups/browse-snapshots.png" alt="Snapshot list showing paginated rows with date, description with cloud upload indicator, scope, warnings, and action buttons" />
<img src="/images/fleet-backups/browse-snapshots.png" alt="Fleet Snapshots list paginated 1 of 5, with rows for a manual snapshot, a Pre-update snapshot: dozzle row, and several Scheduled snapshot: Nightly Snapshot rows, each showing date, scope, warnings, and View, cloud-upload, and delete actions" />
</Frame>
## Snapshot detail view
@@ -60,10 +68,10 @@ The header shows the snapshot's title (or "Untitled Snapshot"), who created it,
Below the header, each node appears as a collapsible card. Expand a node to see its stacks, then expand a stack to see individual files. If documentation was captured for a stack, its preserved Dossier notes are shown read-only beneath the file list. Each file has a **Preview** button that renders the full file contents inline in a scrollable panel, and a **Download** button that saves that single file to your machine (named `<stack>-<filename>`, for example `web-compose.yaml`).
<Frame>
<img src="/images/fleet-backups/snapshot-detail.png" alt="Snapshot detail view with a node expanded, showing a stack expanded with a compose file, Preview button, and Restore button" />
<img src="/images/fleet-backups/snapshot-detail.png" alt="Snapshot detail view for an Untitled Snapshot, with the Local node expanded to its stack list and the plex stack further expanded to show its compose.yaml file with Preview and Download buttons, plus a Restore button per stack and Restore all in the header" />
</Frame>
If any nodes were unreachable during snapshot creation, a warning banner appears at the top of the detail view listing each skipped node and the reason it was skipped. A second banner lists any individual stacks that were only partially captured, naming the node, the stack, and the reason.
Up to four warning banners can appear at the top of the detail view, each only when it applies: skipped nodes that were unreachable during creation, snapshot files that could not be decrypted (see [Storage](#storage) below), stacks that were only partially captured, and, when documentation capture was on, stacks whose Dossier notes could not be fetched. Each banner names the affected node, stack, and reason. A clean snapshot with no problems shows none of them.
## Restoring from a snapshot
@@ -78,7 +86,7 @@ Admins can restore individual stacks from any snapshot:
By default, restoring a stack only writes back its files and leaves the stack's current Dossier notes untouched. Notes are restored only when you explicitly check **Restore documentation notes**, which overwrites the current notes with the captured ones. The same option appears on **Restore all** when the snapshot has documentation.
<Frame>
<img src="/images/fleet-backups/restore-dialog.png" alt="Restore confirmation dialog showing the target stack and node name, a redeploy checkbox, and Cancel and Restore buttons" />
<img src="/images/fleet-backups/restore-dialog.png" alt="Restore plex on Local confirmation dialog with the warning that it overwrites the current compose files with the snapshot version, a Redeploy stack after restore checkbox, and Cancel and Restore buttons" />
</Frame>
Sencho writes the snapshot's files back to the target node:
@@ -109,12 +117,12 @@ Recovery Vault mirrors every fleet snapshot to off-site storage so your snapshot
### Recovery Vault (included)
A managed 500 MB allowance, included with every Admiral subscription. Open **Settings → Infrastructure → Recovery Vault**, choose **Recovery Vault (Included)**, and click **Activate**. Sencho exchanges your license key for scoped storage credentials and starts replicating new snapshots automatically.
A managed 500 MB allowance backed by Cloudflare R2, included with every Admiral subscription. Open **Settings → Infrastructure → Recovery Vault**, choose **Recovery Vault (included)**, and click **Activate**. Sencho exchanges your license key for scoped storage credentials and starts replicating new snapshots automatically.
Once active, the settings page shows a storage gauge (used / 500 MB and object count), a status message confirming auto-upload is on, and a **Reprovision** button to refresh credentials if needed. You can verify connectivity at any time with the **Test** button.
<Frame>
<img src="/images/cloud-backup/cloud-backup-active.png" alt="Recovery Vault settings page with Recovery Vault active, showing the storage gauge, auto-upload status, and the Cloud Snapshots list" />
<img src="/images/cloud-backup/cloud-backup-active.png" alt="Recovery Vault settings page with Storage Mode set to Recovery Vault (included), Test and Reprovision buttons, a storage-used gauge reading 0 Bytes of 500 MB, the auto-upload confirmation message, and an empty Cloud Snapshots panel" />
</Frame>
### Custom S3 (BYOB)
@@ -131,18 +139,18 @@ Bring any S3-compatible bucket: AWS S3, MinIO, Backblaze B2, Wasabi, or your own
Click **Test** to verify connectivity, then **Save**. Secret keys are encrypted at rest. Sencho only sends them to your configured endpoint.
<Frame>
<img src="/images/cloud-backup/cloud-backup-custom.png" alt="Custom S3 configuration form with fields for Endpoint URL, Region, Bucket, Path Prefix, Access Key ID, Secret Access Key, and an Auto-upload toggle" />
<img src="/images/cloud-backup/cloud-backup-custom.png" alt="Custom S3 Configuration form with Storage Mode set to Custom S3 (BYOB) and empty fields for Endpoint URL, Region, Bucket, Path Prefix, Access Key ID, and Secret Access Key, plus an Auto-upload toggle set to off" />
</Frame>
### Manual upload vs auto-upload
When auto-upload is on, every fleet snapshot is replicated as soon as it is created. Manual snapshots from the **Fleet → Snapshots** view upload asynchronously so the UI returns immediately; scheduled snapshots block on the upload so the task's success status reflects cloud durability.
To upload a single snapshot on demand, open the **Snapshots** tab in Fleet View. Each row that hasn't been mirrored yet shows a cloud-upload action in the Actions column. Once a snapshot is in the cloud, an upload icon appears next to its description.
To upload a single snapshot on demand, open the **Snapshots** tab in the Fleet view. Each row that hasn't been mirrored yet shows a cloud-upload action in the Actions column. Once a snapshot is in the cloud, an upload icon appears next to its description.
### Browsing and downloading cloud snapshots
The **Cloud Snapshots** panel in **Settings → Infrastructure → Recovery Vault** lists every archive currently in your bucket, with size and last-modified timestamp. Click the download icon to save a `.tar.gz` archive locally for off-host disaster recovery. Each archive contains a `metadata.json` describing the snapshot and a `nodes/` tree with the captured compose and environment files, organised by node and stack. When the snapshot preserved Dossier notes, the archive also includes a `documentation.json` with those notes.
The **Cloud Snapshots** panel in **Settings → Infrastructure → Recovery Vault** lists every archive currently in your bucket, paginated at 10 per page, with size and last-modified timestamp. Click the download icon to save a `.tar.gz` archive locally for off-host disaster recovery, or the trash icon to permanently delete the archive from your bucket (the corresponding local snapshot in the **Fleet → Snapshots** list is unaffected). Each archive contains a `metadata.json` describing the snapshot and a `nodes/` tree with the captured compose and environment files, organised by node and stack. When the snapshot preserved Dossier notes, the archive also includes a `documentation.json` with those notes.
### Restoring from a cloud snapshot
@@ -150,13 +158,15 @@ For in-place rollback, use the **Restore** action on the snapshot detail view as
## Access control
Every fleet snapshot action requires the **admin** role: viewing the snapshot list, browsing snapshot contents, creating, restoring, deleting, and uploading to the cloud. Because a snapshot captures the `.env` file of every stack, the snapshot list and detail views are restricted to administrators rather than read-only roles.
Every fleet snapshot action requires the **admin** role: viewing the snapshot list, browsing snapshot contents, creating, restoring, deleting, and uploading to the cloud. Because a snapshot captures the `.env` file of every stack, the snapshot list and detail views are restricted to administrators rather than read-only roles. The **Snapshots** tab itself does not appear in the Fleet view for non-admin roles, so there is no read-only path into a stack's `.env` contents.
Recovery Vault configuration is also admin-only. Mirroring to Recovery Vault additionally requires an Admiral subscription; a Custom S3-compatible target works on every tier.
Fleet Snapshots and Recovery Vault settings are both hub-only: the **Snapshots** tab lives inside the **Fleet** view and Recovery Vault lives in **Settings → Infrastructure**, neither of which appears when a remote node is the active selection. See [Multi-Node Management](/features/multi-node) for the full list of hub-only views.
## Storage
Snapshots are stored in Sencho's SQLite database. Captured file contents, including `.env` files, are encrypted at rest with the instance key, so a database copy never exposes stack secrets in plaintext. Compose files are typically small (under 10 KB each), so even hundreds of snapshots consume minimal disk space. Individual files larger than 1 MB are skipped and recorded as a warning to keep snapshots bounded. For very large fleets, consider periodically deleting old snapshots to keep the database lean.
Snapshots are stored in Sencho's SQLite database. Captured file contents, including `.env` files, are encrypted at rest with the instance key, so a database copy never exposes stack secrets in plaintext. Compose files are typically small (under 10 KB each), so even hundreds of snapshots consume minimal disk space. Each captured file is capped at 1 MB to keep snapshots bounded: an oversized `compose.yaml` skips that stack entirely for the snapshot, while an oversized `.env` is dropped and the rest of the stack (including its `compose.yaml`) is still captured. Either case is recorded as a warning, never silently dropped. For very large fleets, consider periodically deleting old snapshots to keep the database lean.
If a stored encrypted file cannot be decrypted (for example after on-disk corruption, including damaged ciphertext that still looks encryption-shaped), the snapshot detail view marks that file unavailable and lists it in a warning. Restore for that stack is blocked so live compose and environment files are never overwritten with that damaged data. Configured off-site upload (Recovery Vault or Custom S3) also refuses to publish an archive that would omit or fabricate those files. Intact stacks in the same snapshot remain readable and restorable. Only short, clearly non-ciphertext values that happen to start with `enc:` (for example `enc:hello`, `enc:hello-world`, or `enc:FOO_BAR=baz`) stay readable as legacy plaintext. Encryption-shaped damage remains unavailable.
@@ -191,3 +201,17 @@ If a stored encrypted file cannot be decrypted (for example after on-disk corrup
Enable **Developer Mode** under **Settings → Operations → Developer Diagnostics** to activate diagnostic logging for snapshot creation (per-node capture timing and file counts), restore operations, and scheduled snapshot execution. Diagnostic logs appear in the server's standard output with a `:debug` suffix.
</Accordion>
</AccordionGroup>
## Where Fleet Backups fits
Fleet snapshots show up in several other views. Each adjacent surface answers a different question.
| Related feature | What it covers | Why it is not Fleet Backups |
|---|---|---|
| [Health-Gated Updates](/features/health-gated-updates) | The readiness check before a stack update or rebuild. | Health-Gated Updates gates the update itself; Fleet Backups is the point-in-time capture it can optionally trigger first. |
| [Storage Portability](/features/compose-storage) | Per-stack mount inventory and portability verdict. | Storage Portability surfaces snapshot recency for stacks with persistent volumes; Fleet Backups owns the actual capture and restore. |
| [Scheduled Operations](/features/scheduled-operations) | Recurring or one-shot scheduled stack operations. | Scheduled Operations owns recurrence and dispatch; Fleet Backups defines what a snapshot captures and how to restore it. |
| [Stack Dossier](/features/stack-dossier) | Operator-authored notes about a stack (purpose, owner, access, recovery). | Stack Dossier owns the notes; Fleet Backups only preserves a read-only copy of them inside a snapshot when documentation capture is on. |
| [Atomic Deployments](/features/atomic-deployments) | The rollback-point mechanism created on every local deploy. | Atomic Deployments backs up automatically on every deploy; Fleet Backups is a separate, on-demand, fleet-wide capture you trigger yourself. |
| [Multi-Node Management](/features/multi-node) | How nodes join the fleet and which top-level views are hub-only. | Multi-Node Management is why Snapshots and Recovery Vault are hub-only; Fleet Backups just inherits that rule. |
| [Licensing](/features/licensing) | Community and Admiral plans. | Licensing covers plan coverage across the product; Fleet Backups only cites the Recovery Vault tier requirement above. |
+14 -2
View File
@@ -25,12 +25,14 @@ homelab-dossier/
- **`index.md`** lists every node with its type, reachability, and stack count, plus an explicit section for any node that was skipped and why.
- **`network.md`** aggregates across the fleet: a **port map**, **volume map**, and **network map**, an **environment checklist** by stack, an **access-URL inventory**, and a **VLAN / static IP / firewall** table. The last two are populated from your Stack Dossier notes.
- **`nodes/<node>.md`** summarises a node and links to its stacks.
- **`stacks/<node>--<stack>.md`** is the same combined document the Stack Dossier's own export produces: generated anatomy followed by your operator notes.
- **`stacks/<node>--<stack>.md`** combines that stack's generated anatomy with your operator notes, in the same format as the Stack Dossier's own export (see below for the one section it omits).
## Generated facts and your notes
Everything Sencho can derive (services, published ports, volumes, networks, restart policy, env file and its variable count, missing `${VAR}` references, and Git source) is generated live from each stack's current Compose file, so it never drifts. Everything Sencho cannot infer (purpose, owner, access URLs, static IP, VLAN, firewall, reverse proxy, backup, upgrade, and recovery notes) comes from the Stack Dossier you maintain per stack. Stacks without notes are still included, documented from their generated facts alone.
When the Networking tab capability is available on a node, each of that node's stack pages also includes a network exposure summary (stack-level exposure intent and per-service port reachability), the same section the [Stack Dossier](/features/stack-dossier) export shows. The Fleet Dossier does not include the Storage tab's storage summary, even when that capability is available and a stack's own Stack Dossier export would show it; download that stack's individual dossier if you need its storage summary.
## Unreachable nodes
A remote node that is offline or unreachable when you export is never allowed to block the rest of the fleet. It appears in `index.md` under **Skipped nodes** with the reason, gets its own node page marked unreachable, and contributes no stack pages. Everything reachable still exports normally.
@@ -44,9 +46,13 @@ As with the Stack Dossier, the generated facts only ever carry env variable **na
Administrators can export the fleet dossier:
1. Open the **Fleet** view.
2. Click **Export Dossier** in the toolbar.
2. Click **Export Dossier** in the toolbar, next to Refresh.
3. Your browser downloads `homelab-dossier.zip`. Unzip it wherever you keep documentation.
<Frame>
<img src="/images/fleet-dossier/fleet-dossier-toolbar.png" alt="Fleet view toolbar showing the Export Dossier button next to Refresh, above the node grid" />
</Frame>
## Troubleshooting
<AccordionGroup>
@@ -62,4 +68,10 @@ Administrators can export the fleet dossier:
<Accordion title="The same stack name appears under two nodes">
Dossiers and stacks are scoped per node, so a stack with the same name on two nodes exports as two separate pages, each prefixed with its node (for example `local--radarr.md` and `media-node--radarr.md`).
</Accordion>
<Accordion title="A stack page is missing its storage summary">
The Fleet Dossier only ever includes a network exposure summary per stack, never a storage summary, regardless of which capabilities are active on that node. Download the stack's own dossier from its Dossier tab to get the storage summary alongside the same generated facts and notes.
</Accordion>
<Accordion title="The export fails immediately with an error toast">
This means the export could not even enumerate your fleet, for example the local node's connection dropped mid-export or your session expired. Unlike a single unreachable remote node, which is skipped and noted in `index.md`, this stops the whole export. Confirm you are still signed in and the local node is reachable, then try again.
</Accordion>
</AccordionGroup>
+82 -54
View File
@@ -3,57 +3,59 @@ title: "Fleet Federation"
description: "Operator-driven placement controls: cordon nodes and pin blueprints to specific nodes."
---
The **Federation** tab is the placement-control surface for fleets running [Blueprints](/features/blueprint-model). It lets you steer where new deployments land without rewriting selectors or labels: mark a node unschedulable for new work, or force a specific blueprint to remain on a specific node regardless of selector matches. Federation builds on the Blueprints reconciler, so the controls described here only affect blueprint-managed deployments.
The **Federation** tab is the placement-control surface for fleets running [Blueprints](/features/blueprint-model). It lets you steer where new deployments land without rewriting selectors or labels: mark a node unschedulable for new work, or force a specific blueprint to remain on a specific node regardless of selector matches. Federation builds on the Blueprints reconciler, so the controls described here only affect blueprint-managed deployments, and every placement change they cause still has to pass through the same rollout-confirmation gate as any other Blueprint change.
Federation lives under **Fleet → Federation**.
<Frame>
<img src="/images/fleet-federation/federation-tab.png" alt="Fleet → Federation tab on a fleet with one cordoned node. The Cordoned nodes section reads '1 of 7' with the hint 'TOGGLE ON EACH NODE CARD' and lists sencho-test-01 with a REMOTE chip, a timestamp ('since 5/18/2026, 5:50:28 PM'), and the operator-supplied reason 'Patching kernel, back online in 30m'. The Pin policy section below reads 'Force a blueprint onto a specific node, overriding its selector.' and shows the empty-state message 'No blueprints yet. Create one in the Deployments tab to manage placement here.'" />
<Frame caption="Fleet → Federation on a fleet with one cordoned node (Opsix, with a reason) and one blueprint in the Pin policy table.">
<img src="/images/fleet-federation/federation-tab-overview.png" alt="Fleet → Federation tab on a four-node fleet. The Cordoned nodes section reads '1 of 4' with the hint 'TOGGLE ON EACH NODE CARD' and lists Opsix with a remote chip, a timestamp, and the operator-supplied reason 'Draining for docs walkthrough, back online shortly'. The Pin policy section below reads 'Force a blueprint onto a specific node, overriding its selector.' and shows one row for the blueprint docs-federation-demo, its selector summary '1 node', an (unpinned) dropdown, and an Effective column reading '1 node'." />
</Frame>
<Note>
Federation placement controls (cordon, uncordon, pin, and unpin) are available on every tier. Cordon and pin mutations require an admin user role, or the node-admin role for cordon when scoped to that node.
Cordon and pin mutations require an admin user role, or the node-admin role for cordon when scoped to that node.
</Note>
## Placement control, not placement automation
Sencho's blueprint reconciler resolves selectors automatically: when a node grows a matching label, the blueprint deploys; when the label is removed, the blueprint is withdrawn (or `evict_blocked` for stateful workloads). That works well until you need to override the automatic decision: take a node out of rotation for maintenance, keep a stack pinned to one host, or hold ground while you migrate. Federation gives you those overrides as explicit operator actions.
The model is deliberate: Sencho proposes placements; you confirm or override them. The reconciler never moves an existing deployment in response to cordon or pin. Cordon affects only *new* placements, and pin only changes which node the reconciler considers desired. Eviction from non-pinned nodes still flows through the existing state-review and confirmation prompts.
The model is deliberate in two layers. First, Sencho proposes placements; you confirm or override them with cordon and pin. The reconciler never moves an existing deployment in response to cordon or pin by itself: cordon affects only *new* placements, and pin only changes which node the reconciler considers desired. Second, changing what the reconciler *wants* to do is not the same as authorizing it to act. Every Blueprint carries a rollout-approval gate ([Blueprints § Apply on demand](/features/blueprint-model#apply-on-demand)): the reconciler only executes place or remove actions that an operator already reviewed and confirmed in a rollout preview. Pinning or unpinning a blueprint always clears that approval, because the pin is part of what Sencho fingerprints as the blueprint's operational intent. Cordoning or uncordoning a node does not touch the blueprint's approval directly, but uncordoning can surface a brand-new placement that was never part of a confirmed plan, and that new placement waits for confirmation the same way a pin change does.
In practice, Federation hands you four operator decisions:
In practice, Federation hands you four operator decisions, each of which still ends at a rollout preview before the fleet actually changes:
- **Don't schedule new work here for a while.** Cordon a node.
- **Anchor this blueprint to one host.** Pin a blueprint.
- **Hand the decision back to the reconciler.** Uncordon or unpin.
- **Ride out a migration.** Combine pin (on the destination) with cordon (on the source) so the source drains naturally and the destination becomes the new home.
- **Anchor this blueprint to one host.** Pin a blueprint, then confirm the rollout.
- **Hand the decision back to the reconciler.** Uncordon or unpin, then confirm the rollout if it surfaces a new placement.
- **Ride out a migration.** Combine pin (on the destination) with cordon (on the source) so the source drains naturally and the destination becomes the new home once you confirm.
## Key capabilities
### Cordon a node
Cordon marks a node as unschedulable. From the moment a node is cordoned, the reconciler skips it for new placements; existing stacks keep running, drift checks keep running, and revision bumps still redeploy in place. The cordon is visible to every tier (the read-only `Cordoned` pill on the node card) so operators can see why a node is not picking up new work. Uncordon to lift the restriction.
Cordon marks a node as unschedulable. From the moment a node is cordoned, the reconciler stops proposing it for new placements: existing stacks keep running, drift checks keep running, and revision bumps still redeploy in place on that node. Because cordon only *removes* a node from consideration, it needs no separate confirmation; the effect is visible on the node's next reconciliation tick without an Apply now / Confirm Apply round trip. The cordon is visible to every role (the read-only `Cordoned` pill on the node card) so operators can see why a node is not picking up new work. Uncordoning is different: it can make the node newly eligible for a blueprint that targets it, and that eligibility is a real fleet mutation, so it goes through the same rollout preview as any other new placement (see [Behaviour and lifecycle](#behaviour-and-lifecycle) below).
The cordon reason is free-form text (up to 256 characters). It surfaces in the Federation tab summary, in the cordon pill's tooltip on the node card, and on the audit log row for the action.
The cordon reason is free-form text (up to 256 characters). It surfaces in the Federation tab summary and in the cordon pill's tooltip on the node card. The action itself is recorded in the audit log with the summary "Cordoned node" or "Uncordoned node."
### Pin a blueprint to a node
Pinning a blueprint forces the reconciler to treat that blueprint as desired only on a single specific node, regardless of what its selector says. The pin overrides the selector entirely. Pin is the right tool when a workload depends on local state, must run on a specific host (a gateway, an integration target), or is in the middle of a host-to-host migration.
When the target node is removed from the fleet, the pin clears automatically and the blueprint reverts to its selector behaviour on the next reconciliation tick.
Selecting a node from the **Pinned to** dropdown saves the pin immediately, but pinning also clears the blueprint's rollout approval: the reconciler will not create the deployment on the new target, or remove it from any node the pin no longer covers, until you open that blueprint in **Fleet → Deployments** and confirm a rollout preview. The Federation tab's **Effective** column shows what the reconciler will pursue once authorized, which is not necessarily what is running right now if a confirmation is still pending.
When the target node is removed from the fleet, the pin clears automatically and the blueprint reverts to its selector behaviour, again subject to a fresh confirmation before anything actually redeploys.
### Audit visibility
Cordon, uncordon, and pin actions all flow through Sencho's standard audit log. Each action records the actor, the affected resource, and the optional reason (for cordon). Filter the **Audit** view by `node.cordon`, `node.uncordon`, or `blueprint.pin` to see the full history.
Cordon, uncordon, and pin all flow through Sencho's standard audit log, alongside every other mutating request. Each row records the actor, the HTTP method and path, and a short summary: "Cordoned node," "Uncordoned node," or "Updated blueprint pin." There is no separate action-name filter for these events; use the free-text search box on the **Audit** view (it matches against the path and the summary) to find them, for example by searching "cordon" or "pin."
## Prerequisites
| Requirement | Why it matters |
|-------------|----------------|
| **Community or Admiral on the control instance** | Federation placement is part of the Compose-first control plane. Remote nodes inherit the control instance tier through the [proxy tier assertion](/features/multi-node#license-enforcement-across-nodes). |
| **Admin or node-admin role** | Pin policy edits require admin. Cordon and uncordon require `node:manage` (admin, or node-admin when scoped to that node). Operator and viewer roles can read cordon state but cannot toggle it. |
| **At least one Blueprint defined** | The Pin policy table is empty until you create a blueprint under **Fleet → Deployments**. Cordon does not require any blueprints; it only suppresses *new* placements from blueprints that exist later. |
| **Active connection to each remote node** | Cordon state is written to the control instance's local database, but it only takes effect once the reconciler runs against the fleet's current node set. A remote node that is `Offline` still carries its cordon flag and resumes honouring it as soon as it comes back. |
| **A confirmed rollout after any pin change** | Setting or clearing a pin only declares intent. The actual create/remove on the fleet waits for **Apply now → Confirm Apply** on the blueprint's detail sheet, same as any other change to the blueprint's operational intent. See [Blueprints § Apply on demand](/features/blueprint-model#apply-on-demand). |
| **Active connection to each remote node** | Cordon state is written to the control instance's local database, but it only takes effect once the reconciler runs against the fleet's current node set. A remote node that is `Offline` still carries its cordon flag and resumes honouring it as soon as it comes back. An offline node also blocks Confirm Apply on any pin that targets it, since the rollout preview elevates an unreachable target to a blocker. |
## Step by step
@@ -61,23 +63,19 @@ Cordon, uncordon, and pin actions all flow through Sencho's standard audit log.
Open **Fleet → Overview**. On the card for the node you want to cordon, click the kebab menu (`⋯`) in the top-right corner and choose **Cordon node**.
<Frame>
<img src="/images/fleet-federation/node-card-kebab-cordon.png" alt="Fleet Overview grid with the kebab menu open on a sencho-test-01 node card. The menu surfaces three items: Edit node, Delete node, and Cordon node (Ban icon). Other node cards (Local, Opsix, Pitt-Moba, sencho-pilot-test, sencho-test-02, SLX-Mars) are visible behind the open menu." />
</Frame>
A confirmation dialog opens with an optional reason field. The reason is free-form text up to 256 characters; type a short note that will help the rest of your team understand why the node is out of rotation.
<Frame>
<img src="/images/fleet-federation/cordon-modal-with-reason.png" alt="Cordon confirmation dialog titled 'Cordon sencho-test-01'. The description reads 'Mark this node as unschedulable. New blueprint deployments will skip it. Existing deployments remain in place.' A 'Reason (optional)' text field is populated with 'Patching kernel, back online in 30m'. Cancel and 'Cordon node' buttons sit in the dialog footer." />
<Frame caption="Cordon confirmation dialog with a reason filled in.">
<img src="/images/fleet-federation/cordon-confirm-dialog.png" alt="Cordon confirmation dialog titled 'Cordon Opsix'. The description reads 'Mark this node as unschedulable. New blueprint deployments will skip it. Existing deployments remain in place.' A 'Reason (optional)' text field is populated with 'Draining for docs walkthrough, back online shortly'. Cancel and 'Cordon node' buttons sit in the dialog footer." />
</Frame>
Click **Cordon node** to commit. The card immediately gains a `Cordoned` pill, and the Federation tab's Cordoned nodes summary picks the node up on the next refresh.
<Frame>
<img src="/images/fleet-federation/node-card-cordoned-badge.png" alt="Fleet Overview grid with the sencho-test-01 node card showing the amber 'Cordoned' pill alongside the existing Online, remote, version, and Update available badges. Surrounding node cards (Local, Opsix, Pitt-Moba, sencho-pilot-test, sencho-test-02, SLX-Mars) have no cordon indicator." />
<Frame caption="Node card carrying the Cordoned pill after confirming.">
<img src="/images/fleet-federation/node-card-cordoned-badge.png" alt="Fleet Overview grid with the Opsix node card showing the amber 'Cordoned' pill alongside the existing Online, remote, and version badges. The Local, Pitt-Moba, and SLX-Mars cards have no cordon indicator." />
</Frame>
Use **Uncordon node** from the same menu to lift the restriction. Uncordoning opens a short confirmation that reads *"Re-enable this node for new blueprint placements. Existing deployments are unchanged."* Confirming clears the flag and the pill disappears on the next refresh.
Use **Uncordon node** from the same menu to lift the restriction. Uncordoning opens a short confirmation that reads *"Re-enable this node for new blueprint placements. Existing deployments are unchanged."* Confirming clears the flag and the pill disappears on the next refresh. If a blueprint's selector now matches the newly uncordoned node, that new placement waits in a `reapproval required` state until you confirm it in the blueprint's rollout preview.
### Pin a blueprint to a node
@@ -87,41 +85,61 @@ Open **Fleet → Federation** and find the blueprint row in the **Pin policy** t
|---|---|
| **Blueprint** | Name and short description. |
| **Selector** | The selector you would otherwise match against, kept for context. |
| **Pinned to** | A dropdown listing every node in the fleet plus an *(unpinned)* option. Changing it saves immediately and triggers a reconciliation. |
| **Effective** | The desired set the reconciler will actually use: the pinned node when set, the selector summary otherwise. |
| **Pinned to** | A dropdown listing every node in the fleet plus an *(unpinned)* option. A cordoned node is labelled inline (`Node · cordoned`) so you can see at a glance that pinning it will override the cordon. |
| **Effective** | The desired set the reconciler will pursue once authorized: the pinned node when set, the selector summary otherwise. This is a live computation, not a status of what is currently deployed. |
Select a node from the **Pinned to** dropdown to pin. Select *(unpinned)* to clear the pin and hand the decision back to the reconciler. The change saves on selection (no separate Save button), and the **Effective** column updates in place.
<Frame caption="The Pinned to dropdown open, with the cordoned node labelled inline.">
<img src="/images/fleet-federation/pin-dropdown-cordoned-hint.png" alt="Pinned to dropdown open for the docs-federation-demo blueprint, listing (unpinned), Local, 'Opsix · cordoned', Pitt-Moba, and SLX-Mars as options." />
</Frame>
When a pin is active, the blueprint's [drift mode](/features/blueprint-model#drift-policy) and stateful classification still apply on the pinned node. Pin only changes *where* the blueprint is desired, not *how* it is reconciled there. The blueprint detail sheet renders a small read-only banner (`Pinned to <node>. Selector is overridden.`) and the deployment table marks the pinned row with a `Pinned` indicator so the override is obvious anywhere the blueprint surfaces.
Select a node from the **Pinned to** dropdown to pin. Select *(unpinned)* to clear the pin and hand the decision back to the reconciler. The selection itself saves immediately, a toast confirms it, and the **Effective** column updates in place.
<Frame caption="Pin saved. A toast confirms the write and the Effective column now reads the pinned node.">
<img src="/images/fleet-federation/pin-applied-toast.png" alt="Federation tab with a success toast reading 'docs-federation-demo pinned to Opsix'. The Pin policy row's Pinned to cell now reads 'Opsix · cordoned' and the Effective column reads 'pin: Opsix'." />
</Frame>
Saving the pin does **not** deploy anything yet. Go to **Fleet → Deployments**, open the blueprint, and click **Apply now**. Sencho opens a rollout preview listing the place and remove actions the pin change produces, any warnings or blockers, and a running Safe / Warnings / Blockers count. Click **Confirm Apply** to authorize and execute the plan.
<Frame caption="Rollout preview for a first, unblocked deploy: one safe placement, one warning, zero blockers.">
<img src="/images/fleet-federation/rollout-preview-safe.png" alt="Confirm rollout dialog for blueprint docs-federation-demo reading 'Enabled blueprints still need this confirmation before the reconciler mutates the fleet.' Summary line reads Safe 1, Warnings 1, Blockers 0, pending. A Warnings section lists 'no persistent volumes detected'. A Changes section lists 'Local (local/online) · create · safe: New placement'. Cancel and Confirm Apply buttons sit in the footer, Confirm Apply enabled." />
</Frame>
If the pin target is unreachable, the preview elevates that placement to a blocker and disables **Confirm Apply** until the node is reachable again or you change the pin:
<Frame caption="Rollout preview after pinning to an offline node: the new placement is a blocker, so Confirm Apply is disabled.">
<img src="/images/fleet-federation/rollout-preview-blocked.png" alt="Confirm rollout dialog reading Safe 0, Warnings 2, Blockers 1, pending. A Blockers section reads 'demo-remote: New placement [remote/unknown: Remote node cached as offline or unknown]'. A Warnings section lists 'Local: Deploy in flight (leaving selector)' and 'no persistent volumes detected'. The Changes list shows the in-flight Local row and the blocked demo-remote create. The Confirm Apply button is greyed out and disabled." />
</Frame>
When a pin is active and confirmed, the blueprint's [drift mode](/features/blueprint-model#drift-policy) and stateful classification still apply on the pinned node. Pin only changes *where* the blueprint is desired, not *how* it is reconciled there. The blueprint detail sheet renders a small read-only banner (`Pinned to <node>. Selector is overridden.`) and the deployment table marks the pinned row with a `Pinned` indicator so the override is obvious anywhere the blueprint surfaces.
### Verify
After cordoning or pinning, check the verification surfaces:
- **Cordon:** the node card carries a `Cordoned` pill in the Fleet → Overview grid, and the node appears in the Federation tab's *Cordoned nodes* section with the timestamp and reason. The audit log records a `node.cordon` event.
- **Pin:** the Pin policy row's **Effective** column reads the pinned node, the blueprint detail sheet shows the override banner, and the deployment table marks the pinned row. The audit log records a `blueprint.pin` event.
- **Cordon:** the node card carries a `Cordoned` pill in the Fleet → Overview grid, and the node appears in the Federation tab's *Cordoned nodes* section with the timestamp and reason. The audit log records a "Cordoned node" entry.
- **Pin:** the Pin policy row's **Effective** column reads the pinned node, the blueprint detail sheet shows the override banner, and the deployment table marks the pinned row once the rollout has been confirmed. Until it is confirmed, the blueprint's catalog tile and detail sheet footer show **pending** or **reapproval required**. The audit log records an "Updated blueprint pin" entry for the pin write itself.
## Behaviour and lifecycle
| Action | Takes effect | Persists across restart |
|---|---|---|
| Cordon | Next reconciliation tick, for new placement and state-review decisions only | Yes |
| Uncordon | Next reconciliation tick | Yes (the flag is cleared, not just hidden) |
| Pin | Immediate; the next reconciliation tick treats the pinned node as the only desired target | Yes |
| Unpin | Immediate; the reconciler reverts to selector behaviour | Yes (cleared) |
| Pinned node deleted | Pin clears automatically as part of node deletion housekeeping | Yes (cleared) |
| Action | Takes effect | Requires a confirmed rollout | Persists across restart |
|---|---|---|---|
| Cordon | Next reconciliation tick, for new placement and state-review decisions only | No, cordon only removes a candidate placement | Yes |
| Uncordon | Node becomes newly eligible on the next tick | Yes, if that eligibility produces a new create/update action | Yes (the flag is cleared, not just hidden) |
| Pin | Saved immediately; clears the blueprint's rollout approval | Yes, always, because the pin is part of the blueprint's fingerprinted intent | Yes |
| Unpin | Saved immediately; clears the blueprint's rollout approval | Yes, always, for the same reason | Yes (cleared) |
| Pinned node deleted | Pin clears automatically as part of node deletion housekeeping | Yes, for whatever placement change follows | Yes (cleared) |
The reconciler only re-evaluates cordon when it is deciding where a blueprint should run *next* or whether the live state matches the desired state. Drift checks against existing containers, revision-driven redeploys, and manual deploys initiated from the stack editor all bypass the cordon flag by design. The flag is a hint to the placement layer, not a quarantine on the node.
The **Effective** column in the Pin policy table is computed live each time the page loads. It is not stored; only the pin itself persists.
The **Effective** column in the Pin policy table is computed live each time the page loads. It is not stored, and it does not mean "currently running": it shows the node the reconciler will pursue once it has an authorized plan to act on. Only the pin itself, and the blueprint's approval state, persist.
## Security and audit
Federation mutations require an admin user role, or the node-admin role for cordon when scoped to that node. The `Cordoned` pill on the node card stays visible at every tier as a read-only signal, so operators without placement permissions can still see why a node is skipping new work.
Federation mutations require an admin user role, or the node-admin role for cordon when scoped to that node. The `Cordoned` pill on the node card stays visible at every role as a read-only signal, so operators without placement permissions can still see why a node is skipping new work.
Every cordon, uncordon, and pin action is captured in the audit log. Each row carries the actor, the action (`node.cordon`, `node.uncordon`, `blueprint.pin`), the affected resource, and the cordon reason where applicable. Filter the **Audit** view by these action names for the full history.
Every cordon, uncordon, and pin write is captured in the audit log with the actor, the request path, the status code, and a short summary. There is no dedicated action-name taxonomy for these events (no `node.cordon` or `blueprint.pin` filter); search the **Audit** view's free-text box for "cordon" or "pin" to find the relevant rows, or narrow by the `POST`/`PUT` method filter.
Federation is hub-only. The cordon flag and the pin live on the control instance, and the blueprint reconciler that reads them also runs on the control instance. Cordoning a remote node does not require any change on the remote itself.
Federation is hub-only. The cordon flag and the pin live on the control instance, and the blueprint reconciler that reads them also runs on the control instance. Cordoning a remote node does not require any change on the remote itself. The confirm-before-mutate gate described above is a fleet-wide Blueprints property, not something Federation adds on top; Federation's cordon and pin writes are simply two of the inputs that can change what a blueprint's next confirmed rollout will do.
## Limitations and non-goals
@@ -129,45 +147,52 @@ Federation v1 ships the two controls above and nothing more. The following are d
- **Drain node.** Evacuating all blueprints from a node depends on volume migration, which is operator-driven for stateful workloads in the current release.
- **Capacity planning.** Predictive resource utilisation belongs to a later iteration once there is real-world fleet usage data to calibrate against.
- **Auto-eviction on cordon.** Cordon never withdraws or evicts an existing deployment. Use the deployment table for explicit withdraw or eviction confirmations.
- **In-place-redeploy block.** Cordon does not stop revision-driven redeploys on the cordoned node. Bumping a blueprint's revision still redeploys the existing stack in place on every node where it already runs, cordoned or not.
- **Auto-eviction on cordon.** Cordon never withdraws or evicts an existing deployment. Use the deployment table for explicit withdraw or eviction confirmations, which themselves require an authorized remove outcome from a confirmed rollout.
- **In-place-redeploy block.** Cordon does not stop revision-driven redeploys on the cordoned node. Bumping a blueprint's revision still redeploys the existing stack in place on every node where it already runs, cordoned or not, once that revision change is confirmed.
- **Manual deploy interception.** Cordon affects the blueprint reconciler only. A manual deploy initiated from the stack editor still lands on whichever node you target.
- **Drift-mode override.** Pin does not change the blueprint's drift policy or stateful classification on the pinned node. Both flow through unchanged.
- **Automatic pin execution.** Pinning or unpinning never deploys or withdraws anything by itself. It only changes what the next rollout preview will propose; you still confirm it.
- **Reason length.** The cordon reason is capped at 256 characters and stored verbatim.
## Practical workflows
### Take a node out of rotation for OS patching
Cordon the node with a reason that names the work (e.g. *"Patching kernel, back online in 30m"*). The Federation tab summary and the node-card tooltip surface that reason for the rest of your team. Existing stacks keep running while you reboot and patch; new blueprint placements skip the node until you uncordon.
Cordon the node with a reason that names the work (e.g. *"Patching kernel, back online in 30m"*). The Federation tab summary and the node-card tooltip surface that reason for the rest of your team. Existing stacks keep running while you reboot and patch; new blueprint placements skip the node until you uncordon, and even then nothing deploys onto it until you confirm the resulting rollout.
### Migrate a stack between hosts
Pin the blueprint to the destination node first. The next reconciliation tick deploys onto the destination. Once health checks pass on the destination, withdraw the original deployment from the source via the deployment table. The pin holds the workload on the destination while you tear down the source, so there is no window where the reconciler can reverse the move.
Pin the blueprint to the destination node, then open the blueprint in **Fleet → Deployments** and confirm the rollout preview. That single confirmation both authorizes the new placement on the destination and authorizes withdrawing (or, for a stateful blueprint, flagging for eviction) the node the pin no longer covers. Once health checks pass on the destination, withdraw or evict the original deployment from the deployment table if it did not clear automatically. The pin holds the workload on the destination while you tear down the source, so there is no window where the reconciler can reverse the move on its own; the only remaining step is the explicit confirmation you already made.
### Anchor a gateway-only blueprint
Pin a gateway blueprint (reverse proxy, ingress, tunnel) to the node that owns the public IP. Even if the selector matches a second node, the pin overrides; only the gateway host receives the deployment. If the gateway node is also cordoned for maintenance, the pin wins: the blueprint stays on the pinned node even while it is otherwise unschedulable.
Pin a gateway blueprint (reverse proxy, ingress, tunnel) to the node that owns the public IP, then confirm the rollout. Even if the selector matches a second node, the pin overrides; only the gateway host receives the deployment. If the gateway node is also cordoned for maintenance, the pin wins: the blueprint stays on the pinned node even while it is otherwise unschedulable.
## Pin overrides cordon
Cordon governs automatic placement; pin is an explicit operator decision. When the two collide (a blueprint pinned to a cordoned node), pin wins. The blueprint stays on (or deploys onto) the pinned node even though the node is otherwise unschedulable. This keeps cordon's cost predictable: cordoning a node cannot silently break a workload you already chose to anchor there. If you want to remove the workload too, unpin the blueprint or withdraw it explicitly.
Cordon governs automatic placement; pin is an explicit operator decision. When the two collide (a blueprint pinned to a cordoned node), pin wins. The blueprint stays on (or, once confirmed, deploys onto) the pinned node even though the node is otherwise unschedulable. This keeps cordon's cost predictable: cordoning a node cannot silently break a workload you already chose to anchor there. If you want to remove the workload too, unpin the blueprint or withdraw it explicitly (subject to the same confirmation rule as any other removal).
### Pinning a stateful blueprint
Pinning a stateful blueprint that is currently deployed on multiple nodes shrinks the desired set to one node. On the next reconciliation tick, the non-pinned deployments enter `evict_blocked` and wait for an explicit eviction confirmation. This is the same flow that protects stateful workloads from automatic withdraw when a selector changes. Confirm each eviction from the deployment table or unpin the blueprint to restore the original desired set.
Pinning a stateful blueprint that is currently deployed on multiple nodes shrinks the desired set to one node. Once you confirm the resulting rollout, the non-pinned deployments enter `evict_blocked` and wait for an explicit eviction confirmation from the deployment table. This is the same flow that protects stateful workloads from automatic withdraw when a selector changes. Confirm each eviction from the deployment table or unpin the blueprint to restore the original desired set.
## Troubleshooting
<AccordionGroup>
<Accordion title="I pinned a blueprint (or uncordoned a node) but nothing deployed">
This is expected. Pinning, unpinning, and any placement change that uncordoning surfaces only update what the reconciler *wants* to do; they never execute on their own. Open the blueprint in **Fleet → Deployments**, click **Apply now**, review the rollout preview, and click **Confirm Apply**. Until you do, the blueprint's catalog tile and detail sheet footer show **pending** or **reapproval required**.
</Accordion>
<Accordion title="Confirm Apply is disabled in the rollout preview">
A blocker is present. The most common cause after a pin change is an offline or unreachable target node: the preview elevates a create or remove action on an offline node to a blocker and disables Confirm Apply until it clears. Check the node's status in Fleet → Overview, or change the pin to a reachable node. An in-progress deploy on another node (`Deploy in flight`) surfaces as a warning, not a blocker, and does not by itself block confirmation.
</Accordion>
<Accordion title="A blueprint refuses to deploy on a node that matches its selector">
Check whether the node is cordoned. Cordoned nodes are excluded from new placements; the Federation tab summary lists every cordoned node and the reason. Uncordon the node to re-enable automatic placement, or pin the blueprint explicitly to deploy onto a cordoned node.
Check whether the node is cordoned. Cordoned nodes are excluded from new placements; the Federation tab summary lists every cordoned node and the reason. Uncordon the node to make it eligible again, then confirm the resulting rollout preview, or pin the blueprint explicitly to deploy onto a cordoned node (also subject to confirmation).
</Accordion>
<Accordion title="A cordoned node still received a new deployment">
Check whether the blueprint is pinned to that node. Pin overrides cordon by design, so a blueprint pinned to a cordoned node still deploys on the next reconciliation tick. Unpin the blueprint to restore selector-driven placement, then cordon will be honoured.
Check whether the blueprint is pinned to that node. Pin overrides cordon by design, so a blueprint pinned to a cordoned node still deploys once the pin's rollout is confirmed. Unpin the blueprint to restore selector-driven placement, then cordon will be honoured.
</Accordion>
<Accordion title="A pinned blueprint deployed somewhere unexpected">
The pin is the source of truth. Open Federation and confirm the **Pinned to** value matches your intent. The **Effective** column shows what the reconciler will use. Selector matches are ignored while a pin is set.
The pin is the source of truth. Open Federation and confirm the **Pinned to** value matches your intent. The **Effective** column shows what the reconciler will pursue once authorized. Selector matches are ignored while a pin is set.
</Accordion>
<Accordion title="Pinning a blueprint left rows in evict_blocked on the other nodes">
That is the stateful guard working as intended. The reconciler does not auto-evict stateful workloads; each leftover deployment must be confirmed from the deployment table, just like a selector change would require. Unpinning the blueprint restores the original desired set if you want to keep all of them.
@@ -176,13 +201,16 @@ Pinning a stateful blueprint that is currently deployed on multiple nodes shrink
The Fleet Overview grid polls every 30 seconds, so the `Cordoned` pill can lag the action by up to that long. The Federation tab summary and the audit log update immediately; if the audit log shows the cordon action but the badge is still missing, click **Refresh** on the Fleet page to force an immediate re-fetch.
</Accordion>
<Accordion title="The Federation tab is not visible">
Federation is available on every tier under **Fleet → Federation**. Cordon and pin mutations require an admin user (or node-admin for cordon on nodes they manage). If the tab is missing, refresh the Fleet page; if controls are read-only, sign in as an admin.
Federation lives under **Fleet → Federation** on every Sencho instance. Cordon and pin mutations require an admin user (or node-admin for cordon on nodes they manage). If the tab is missing, refresh the Fleet page; if controls are read-only, sign in as an admin.
</Accordion>
<Accordion title="The Pin policy table is empty even though I created blueprints">
Federation reads from the same Blueprints registry that powers **Fleet → Deployments**. If the Deployments tab shows blueprints but Federation does not, refresh the Fleet page (the Pin policy table caches its source list when the tab mounts). If Deployments is also empty, no blueprint has been saved yet; create one there first.
</Accordion>
<Accordion title="A pinned node was deleted">
The pin clears automatically when its target node is removed from the fleet. The blueprint reverts to its selector behaviour on the next reconciliation tick. There is no manual cleanup step.
The pin clears automatically when its target node is removed from the fleet. The blueprint reverts to its selector behaviour, subject to a fresh rollout confirmation before anything actually redeploys. There is no manual cleanup step.
</Accordion>
<Accordion title="I can't find 'node.cordon' or 'blueprint.pin' in the Audit filters">
Those action names don't exist. Sencho's audit log records the HTTP method, path, and a plain-language summary ("Cordoned node," "Uncordoned node," "Updated blueprint pin") rather than a fixed action taxonomy. Use the Audit view's search box with a keyword like "cordon" or "pin" instead of a formal filter value.
</Accordion>
</AccordionGroup>
@@ -198,7 +226,7 @@ Federation is one tab in a larger Fleet view, and it focuses on a narrow slice o
| [Sencho Mesh](/features/sencho-mesh) | Cross-node container networking so a service on one node can reach a service on another by alias. | Mesh routes packets between containers; Federation decides which nodes receive blueprint deployments. |
| [Fleet Actions](/features/fleet-actions) | Bulk operations across labelled nodes (restart, stop). | Fleet Actions runs imperative operations on existing deployments; Federation steers declarative placement decisions. |
| [Fleet Sync](/features/fleet-sync) | Push-only replication of security policies (scan policies, CVE suppressions) from a control instance to replica instances. | Fleet Sync replicates *security state*, not placement; the two features do not interact. |
| [Blueprints](/features/blueprint-model) | The declarative deployment model whose reconciler Federation steers. | Required reading: without Blueprints, Federation has nothing to do. |
| [Blueprints](/features/blueprint-model) | The declarative deployment model whose reconciler Federation steers, including the rollout-preview and Confirm Apply mechanic every Federation change flows through. | Required reading: without Blueprints, Federation has nothing to do, and the confirm step described throughout this page is defined there. |
| [Licensing](/features/licensing) | Community and Admiral plans. | How fleet capabilities relate to plans. |
Federation is not Fleet Sync, not Mesh, and not the remote-node proxy. The cordon flag affects only declarative blueprint deployments, not manually deployed stacks. If you are looking for a way to take an entire node fully out of service (existing deployments included), see the deployment table's withdraw flow on each affected blueprint; cordon by itself is intentionally non-destructive.
+24 -10
View File
@@ -11,10 +11,10 @@ Fleet Sync replicates three resources today, all over the same channel and with
- **CVE suppressions** ([CVE Suppressions](/features/cve-suppressions)).
- **Misconfig acknowledgements**.
Fleet Sync lives on the **Security** page (the **Policies** and **Suppressions** tabs) on both the control (where you author rules) and the replica (where you see them, read-only).
Fleet Sync lives on the **Security** page (the **Policies** and **Suppressions** tabs, the latter also holding misconfig acknowledgements) on both the control (where you author rules) and the replica (where you see them, read-only).
<Frame>
<img src="/images/fleet-sync/control-security.png" alt="The Security page on the control. The Scanner setup tab shows Trivy installed with Auto-update Trivy turned on. The Policies tab shows the empty 'No scan policies configured' state, and the Suppressions tab lists two active suppressions against github.com/docker/docker, each annotated 'by admin · expires Never'." />
<img src="/images/fleet-sync/fleet-sync-policies-control.png" alt="The Security page Policies tab on the control instance. The header reads 'Deploy enforcement policies' with an Add policy button, the body shows the empty 'No scan policies configured' state, and a Honor suppressions in deploy blocks toggle sits below, switched off." />
</Frame>
## Control and replica roles
@@ -28,18 +28,32 @@ Every Sencho instance carries a `fleet_role` flag that is either `control` or `r
The transition from control to replica happens automatically the first time a replica accepts a push: the apply transaction sets `fleet_role = 'replica'` atomically with the row replacement, so the role flip and the new rows land together or not at all. Going the other way is explicit: an admin clicks **Demote to control** on the replica (see [Demote a replica](#demote-a-replica) below).
<Frame>
<img src="/images/fleet-sync/fleet-sync-policies-replica.png" alt="The Security page Policies tab on a replica, signed in directly. A callout reads 'Managed by control node: Security policies replicate from the control Sencho instance. View them here for audit; edit them on the control,' with a Demote to control button, above the empty 'No scan policies configured' state." />
</Frame>
### Viewing a replica through the control's fleet view
The read-only banner above only appears when you sign in **directly** to the replica's own URL. Scan policies, CVE suppressions, and misconfig acknowledgements are fetched with a `localOnly` request, so they are not proxied through the Distributed API the way most other per-node pages are. If you instead browse a remote node's **Security** page from the control by switching nodes in the top-left node switcher, the Policies and Suppressions tabs show a different message and no replicated data at all:
<Frame>
<img src="/images/fleet-sync/fleet-sync-proxy-notice.png" alt="The Security page Policies tab on the control, with the node switcher set to a remote named Opsix. A callout reads 'Managed on the local instance: Scan policies are managed on the local Sencho instance. Switch to the local node to manage them,' with no policy list below it." />
</Frame>
To see a replica's mirrored rules, sign in to that instance's own URL instead of viewing it through the control's node switcher. This is different from most other Security page tabs (Images, Secrets, Scanner setup) and most other Sencho pages generally, which do proxy through the node switcher; scan policies, CVE suppressions, and misconfig acknowledgements are the exception.
## What replicates
The three replicated resources share one wire protocol, one retry queue, and one anchor. Each push carries the full current state of one resource (not a delta); the receiver replaces every `replicated_from_control = 1` row in a single transaction.
The three replicated resources share one wire protocol, one retry queue, and one anchor. Each push carries the full current state of one resource (not a delta); the receiver replaces every `replicated_from_control = 1` row in a single transaction. On the replica, each mirrored row carries a **replicated** tag so you can tell it apart from a local one at a glance:
<Frame>
<img src="/images/fleet-sync/fleet-sync-suppressions-replica.png" alt="The Security page Suppressions tab on a replica. Two CVE suppressions for github.com/docker/docker are listed, each tagged 'Accepted risk' and 'replicated,' with no Add suppression or Export VEX buttons visible. A Misconfig acknowledgements section below is empty." />
</Frame>
What does *not* replicate:
- **Local rules created directly on a replica.** A replica can still hold its own local rules; sync only touches rows that originated on the control. The two coexist in the same table and are distinguished by an internal flag.
- **Trivy itself.** The scanner binary is installed independently on each instance. The Security panel on a remote shows a "Scanner is per-node" callout in place of the full editor, since the scanner lifecycle is a node concern, not a fleet concern.
<Frame>
<img src="/images/fleet-sync/remote-security-via-proxy.png" alt="The Security page Scanner setup tab on a remote, opened via the control's node switcher. The Vulnerability Scanner card shows 'Not installed' with an Install Trivy button; below it, a 'Scanner is per-node' status callout reads 'Trivy is installed independently on each Sencho instance. Scan policies and CVE suppressions are managed on the control node.'" />
</Frame>
- **Trivy itself.** The scanner binary is installed independently on each instance. The Security page's **Scanner setup** tab (unlike Policies and Suppressions, this one does proxy through the node switcher) shows a "Scanner is per-node" callout in place of the full editor when you view it on a remote, since the scanner lifecycle is a node concern, not a fleet concern.
- **Everything outside the three resources above.** API tokens, audit logs, blueprints, secrets, alert rules, users, SSO config, and general settings stay per-instance.
- **Pilot-agent nodes.** Sync over the [pilot tunnel](/features/pilot-agent) is not part of v1; the control logs a one-time warning per pilot node and skips it during fanout. The pilot node's local rules are unaffected.
@@ -124,7 +138,7 @@ Demote requires `{"confirm": true}` in the request body to prevent a misclick fr
Fleet Sync v1 ships the three replicated resources and the control mechanics described above. The following are deliberately out of scope today:
- **No first-party sync-status panel in the UI.** Sync activity is visible in two places: the receiving replica's audit log (every applied push records `POST /api/fleet/sync/<resource>` with username `system` and the source fingerprint in the summary) and the control's server logs. A built-in status panel is on the post-1.0 roadmap.
- **The built-in status signal is a rollup, not a push-by-push history.** [Fleet View](/features/fleet-view)'s **Status** tab shows a **Policy sync** row per remote (`In sync`, `degraded`, or `paused`), but it only tells you the current state, not the history of individual pushes. For that detail, use the receiving replica's audit log (every applied push records `POST /api/fleet/sync/<resource>` with username `system` and the source fingerprint in the summary) or the control's server logs.
- **Stack-pattern scope only in the form UI.** The scan policy and CVE suppression forms expose a stack-pattern glob (e.g. `prod-*`) but no node selector. The underlying data model supports node-scoped rules, and they replicate faithfully when present, but authoring them today requires writing directly to the control's API.
- **5,000 rows per resource per push.** If the control accumulates more than 5,000 active rules in any one resource, the sender truncates to the first 5,000 and emits a `warning` notification (cool-down: 6 hours). In practice no fleet should approach this cap; if you see the warning, treat it as a signal to consolidate rules.
- **Pilot-agent nodes are not part of Fleet Sync today.** The control logs a one-time warning per pilot node and skips it during fanout. Replicating security rules over the [pilot tunnel](/features/pilot-agent) is on the post-1.0 roadmap.
@@ -150,7 +164,7 @@ Fleet Sync v1 ships the three replicated resources and the control mechanics des
The push for the delete probably failed. Check **Settings → Nodes** on the control; a remote that is offline or whose token is invalid will not have received the delete. Once the underlying issue is resolved, the next retry catches the remote up to the current state (which no longer includes the deleted rule).
</Accordion>
<Accordion title="I cannot see whether sync succeeded">
There is no first-party sync-status panel in the UI today (see [Limitations](#limitations)). Two surfaces give you the information indirectly: the replica's **Audit** view records every applied push as a `POST /api/fleet/sync/<resource>` entry with username `system` and the source fingerprint in the summary, and the control's server logs record the push outcome per remote. The administrative API endpoint `GET /api/fleet/sync-status` returns the per-resource per-node status table for tooling that needs to consume it directly.
Open the [Fleet View](/features/fleet-view) **Status** tab: each remote's card shows a **Policy sync** row reading `In sync`, `degraded`, or `paused`, with a tooltip naming the last error when degraded. For push-by-push detail beyond that rollup (see [Limitations](#limitations)), two surfaces give you the information directly: the replica's **Audit** view records every applied push as a `POST /api/fleet/sync/<resource>` entry with username `system` and the source fingerprint in the summary, and the control's server logs record the push outcome per remote. The administrative API endpoint `GET /api/fleet/sync-status` returns the per-resource per-node status table for tooling that needs to consume it directly.
</Accordion>
<Accordion title="A warning says my suppression list was truncated">
The control side of Fleet Sync caps each push at 5,000 rows per resource (see [Limitations](#limitations)). If your suppression list, scan policy list, or acknowledgement list exceeds the cap, the sender truncates to the first 5,000 and emits a single warning every 6 hours. Consolidate rules by replacing repetitive entries with pattern-scoped ones, or remove suppressions that are no longer needed.
+35 -26
View File
@@ -3,14 +3,14 @@ title: Fleet View
description: Monitor every node in your Sencho deployment from one screen, drill into stacks and containers, and orchestrate Sencho version updates across the fleet.
---
The **Fleet** tab is the single page you open when you want to see the whole estate at once: every node's health, every stack, every container, and which boxes need a Sencho update. It is the home for fleet-wide tabs that go beyond monitoring (Snapshots, Status, Deployments, Routing, Federation, Actions, Secrets), each linking out to its own dedicated page.
The **Fleet** tab is the single page you open when you want to see the whole estate at once: every node's health, every stack, every container, and which boxes need a Sencho update. It is the home for fleet-wide tabs that go beyond monitoring (Snapshots, Docker Labels, Deployments, Routing, Federation, Actions, Secrets), each linking out to its own dedicated page.
<Note>
Fleet is hub-only and is hidden from the nav strip when a remote node is the active selection. See [Multi-Node Management](/features/multi-node#what-top-level-views-show-when-a-remote-node-is-active).
</Note>
<Frame>
<img src="/images/fleet-view/fleet-overview.png" alt="Fleet page showing the masthead with the editorial 'The fleet' word, a meta line reading '8 nodes · 8 online · last sync 12s', a '1 critical' reasons line, and CPU / MEM / CONTAINERS stat tiles next to a 1-alert counter. Below, the eight-tab strip starts at Overview and ends at Secrets. Under that, the toolbar (search, sort, filters, Grid/Topology toggle) sits above a grid of node cards: a pinned Local card with a cyan ★ Local rail and an Online badge plus version chip, then Opsix, Pitt-Moba, sencho-pilot-test (with an Update available badge and Update to v0.76.3 button), and four more remote cards." />
<img src="/images/fleet-view/fleet-overview.png" alt="Fleet page showing the masthead with the editorial 'The fleet' word, a meta line reading '4 nodes · 4 online · last sync 5s', and CPU / MEM / CONTAINERS stat tiles next to a 0-alert counter. Below, the tab strip runs Overview, Snapshots, Status, Map, Docker Labels, Deployments, Federation, Actions. Under that, the toolbar (collapsed search icon, sort, filters, Grid/Topology toggle, Node Update, Manage nodes) sits above a grid of four node cards: a pinned Local card with a cyan ★ Local rail, an Online badge, a version chip, and a 'Networking · exposed' pill, followed by Opsix, Pitt-Moba, and SLX-Mars." />
</Frame>
## Page layout
@@ -21,9 +21,8 @@ The page has three rails stacked top to bottom: a status masthead, a tab strip p
A single rail summarises the state of every registered node so you can read the whole estate without scrolling.
- A **state word** that always reads `The fleet`, set in the editorial italic display face. The word changes color with the fleet's overall state: foreground when **Healthy** (every node online, none critical), warning when **Degraded** (any node offline), destructive when **Critical** (any online node above the CPU or disk threshold).
- A **pulsing dot** in the same colour, with a soft halo. The dot is solid when Healthy and pulses when Degraded or Critical, so motion only appears when something needs attention.
- A **rail tint** down the left edge of the masthead that mirrors the state colour, plus a subtle gradient wash across the bar.
- A **state word** that always reads `The fleet`, set in the editorial display face. The word changes color with the fleet's overall state: foreground when **Healthy** (every node online, none critical), warning when **Degraded** (any node offline), destructive when **Critical** (any online node above the CPU or disk threshold).
- A **rail tint** down the left edge of the masthead that mirrors the state colour, plus a subtle gradient wash across the bar. When Healthy, the rail carries a slow ambient shimmer; when Degraded or Critical, it switches to a steady glow instead, so an alert state reads as calm, sustained emphasis rather than a distracting pulse.
- A **meta line** in uppercase mono tracking that reads `<n> nodes · <m> online · last sync Xs`, where the sync delta uses compact units (`s` / `m` / `h`). While data is loading the line reads `syncing…` instead.
- A **reasons line** below the meta line that names exactly what is wrong, e.g. `1 offline · 1 critical`. The reasons line is hidden when the fleet is Healthy.
- Three **stat tiles** on the right edge, in mono tabular numerals:
@@ -38,14 +37,15 @@ A single rail summarises the state of every registered node so you can read the
### Tabs
The Fleet view is a tab strip. Every tier sees Overview, Status, Map, Deployments, Federation, and Actions. Snapshots appears for admins. Routing and Secrets are limited-availability fleet surfaces and are not part of the default tab strip. A vertical separator after **Status** divides the per-node monitoring tabs from the fleet-wide orchestration tabs.
The Fleet view is a tab strip. Every tier sees Overview, Status, Map, Docker Labels, Deployments, Federation, and Actions. Snapshots appears for admins. Routing and Secrets are limited-availability fleet surfaces and are not part of the default tab strip. A vertical separator after **Docker Labels** (or after **Map** when Docker Labels is not present) divides the per-node monitoring tabs from the fleet-wide orchestration tabs.
| Tab | Tier | What it does |
|-----|------|--------------|
| **Overview** | Community | The grid or topology view of every node and its health. Covered in the next section. |
| **Snapshots** | Community | Snapshot every compose file across the fleet. See [Fleet-Wide Backups](/features/fleet-backups). |
| **Snapshots** | Community (admin role) | Snapshot every compose file across the fleet. See [Fleet-Wide Backups](/features/fleet-backups). |
| **Status** | Community | One card per node summarising which automations and security features are configured. Covered below. |
| **Map** | Community | A read-only map of how stacks, services, networks, volumes, and ports relate across the fleet, with anomaly flags. Covered below. |
| **Docker Labels** | Community | Estate-wide audit of Docker and Compose labels across every node. See [Docker Label Audit](/features/docker-label-audit). |
| **Deployments** | Community | Blueprint deployments and reconciler state. See [Blueprints](/features/blueprint-model). |
| **Routing** | Limited availability | Cross-node service routing via Sencho Mesh when that surface is enabled on the instance. See [Sencho Mesh](/features/sencho-mesh). |
| **Federation** | Community | Cordon nodes and pin blueprints to specific hosts. See [Fleet Federation](/features/fleet-federation). |
@@ -54,13 +54,14 @@ The Fleet view is a tab strip. Every tier sees Overview, Status, Map, Deployment
### Action buttons
Three buttons sit next to the tab strip in the top-right corner. They are visible from every tab, not just Overview.
Two buttons sit next to the tab strip in the top-right corner. They are visible from every tab, not just Overview.
| Button | What it does |
|--------|--------------|
| **Check Updates** | Opens the [Node Updates sheet](#node-updates) where you can manage Sencho version updates across the fleet. |
| **Refresh** | Forces an immediate re-fetch of fleet data. The icon spins while the refresh is in flight; the button is disabled until it completes. |
| **Add node** | Opens the same Add Node dialog as **Settings · Infrastructure · Nodes** so you can register a new node without leaving the Fleet page. Admin-only. After saving a remote proxy node, a connection test runs automatically and the result toasts in (success or saved-but-unreachable). |
| **Export Dossier** | Admin-only. Walks every node and stack and downloads a `homelab-dossier.zip` Markdown archive of the whole fleet. See [Fleet Dossier](/features/fleet-dossier). |
The **Node Update** and **Manage nodes** buttons live in the Overview tab's own toolbar rather than this shared row; see [Toolbar](#toolbar) below.
## The Overview tab
@@ -68,15 +69,17 @@ Overview is the default tab and is where most operators spend their time. It off
### Toolbar
In **Grid** mode the toolbar exposes search, sort, filters, and the view toggle. In **Topology** mode only the view toggle remains (the grid-only controls collapse).
In **Grid** mode the toolbar exposes search, sort, filters, and the view toggle. In **Topology** mode only the view toggle remains (the grid-only controls collapse). **Node Update** and **Manage nodes** sit at the end of the row in both modes.
| Control | Behaviour |
|---------|-----------|
| **Search** | Real-time filter that matches against node names and the names of stacks deployed on each node. Typing `plex` keeps only nodes that have a `plex` stack; typing `dev` keeps only nodes whose name contains `dev`. |
| **Search** | Collapsed to a single icon button by default; click it to expand the input (it stays expanded while a query is entered and collapses again on blur once cleared). Filters in real time against node names and the names of stacks deployed on each node. Typing `plex` keeps only nodes that have a `plex` stack; typing `dev` keeps only nodes whose name contains `dev`. |
| **Sort** | Combobox with five orderings: Name, CPU Usage, Memory Usage, Containers, Status. The selection persists in the browser. |
| **Direction toggle** | Arrow button next to the sort combobox. The icon rotates 180° and the title flips between *Switch to ascending* / *Switch to descending* to confirm the current direction. |
| **Filters** | Popover with four sections: **Status** (All / Online / Offline), **Type** (All / Local / Remote), **Severity** (Critical Only toggle), and **Tags** (multi-select of fleet label palette, only visible if labels exist). The button shows a count badge for active filters; a **Clear all filters** action appears at the bottom of the popover when at least one filter is set. |
| **Grid / Topology** | Segmented control on the right, with a Grid icon and a Topology icon. The control resets to Grid when the page is reloaded. |
| **Filters** | Popover with five sections: **Status** (All / Online / Offline), **Type** (All / Local / Remote), **Severity** (Critical Only toggle), **Networking** (All / Exposed / Unknown / Drift, matching the [networking badge](#grid-view) on each card), and **Tags** (multi-select of fleet label palette, only visible if labels exist). The button shows a count badge for active filters; a **Clear all filters** action appears at the bottom of the popover when at least one filter is set. All filter and sort selections persist in the browser. |
| **Grid / Topology** | Segmented control, with a Grid icon and a Topology icon. The control resets to Grid when the page is reloaded. |
| **Node Update** | Opens the [Node Updates sheet](#node-updates) and immediately re-checks every node's version, same as **Recheck** inside the sheet. |
| **Manage nodes** | Admin-only. Navigates to **Settings → Infrastructure → Nodes**, where you register, edit, or remove nodes. |
### Grid view
@@ -89,12 +92,15 @@ Every node renders as a card. The local node is pinned at the top of the grid wi
| **Type badge** | Outline pill reading `local` or `remote`. |
| **Version badge** | The node's Sencho version in mono tabular numerals (e.g. `v0.76.3`). Hidden if the node cannot report a version. |
| **Update available** badge | Warning pill shown when a newer Sencho release is published for this node. |
| **Pinned** badge | Shown instead of **Update available** when the node's own Sencho image is pinned to a digest (or an unrecognised pin) and cannot be updated automatically from here. A tooltip explains why. |
| **Critical** badge | Destructive pill with a triangle icon, surfaced when the online node is above 90% CPU or 90% disk. |
| **Cordoned** badge | Warning pill with a Ban icon, surfaced when the node is cordoned. The badge tooltip carries the cordon reason or the default *Unschedulable: new blueprint deployments skip this node*. See [Fleet Federation](/features/fleet-federation) for the full cordon and pin flow. |
| **Updating / Updated / Failed** badge | Update progress indicator, shown only while or just after an update flows through. Failed states surface inline retry and dismiss buttons and a cursor-following error tooltip. |
| **Networking** badge | Warning pill reading `Networking · exposed`, `Networking · drift`, or `Networking · unknown exposure`, shown when the node has a stack with published ports, detected network drift, or unresolved exposure. Click it to jump to that node's Networking page. |
| **Updating / Updated / Failed / Timed out** badge | Update progress indicator, shown only while or just after an update flows through. Failed and timed-out states surface inline retry and dismiss buttons and a cursor-following error tooltip. |
| **Skipped** badge | Shown on the card when an available update has been deferred from the [Node Updates sheet](#node-updates); the same skip state is reflected there. |
| **Container stats grid** | Three cells: **Running** (active containers), **Stopped** (exited containers), **Stacks** (count, or `-` if the node has not reported). Hidden on offline nodes. |
| **CPU / RAM / Disk bars** | Each row shows the metric icon, the percent (CPU) or `used / total` (RAM, Disk), and a horizontal bar that tints amber at 60%, destructive at 80% (CPU/RAM), or amber at 75% / destructive at 90% (Disk). Hidden on offline nodes. |
| **Update to v…** button | Outline button that runs along the bottom of the card when an update is available. The label includes the latest version. |
| **Update to v…** button | Admin-only outline button that runs along the bottom of the card when an update is available. The label includes the latest version. |
| **Stack details** trigger | Footer button that toggles the stack drill-down. The label carries the stack count for the node. |
Offline nodes render dimmed, with no stats grid, no usage bars, and no update affordance.
@@ -108,25 +114,27 @@ Every card carries a three-dot **Node actions** kebab in the top-right corner. T
| **Edit node** | Opens the Edit dialog prefilled with the node's connection details. For proxy-mode remotes, saving with a changed API URL or token re-runs the connection test automatically. |
| **Delete node** | Opens a destructive confirmation. The local (default) node has no Delete option. Deleting a remote only removes it from this console; the remote instance and its containers are untouched. |
| **Cordon node** / **Uncordon node** | Marks the node unschedulable so new blueprint deployments skip it. Existing deployments keep running. Requires the `node:manage` permission (admin, or node-admin when scoped to that node). |
| **Mute** submenu | Mute node notifications, mute update notifications, mute monitor alerts for this node, or open the full mute-rule manager. Shown to whoever can manage mute rules for the node. See [Alerts & Notifications](/features/alerts-notifications). |
Edit and delete remain admin-only. Users without `node:manage` and without edit/delete affordances see no kebab on the card.
Edit and delete remain admin-only. Users without `node:manage`, without mute permission, and without edit/delete affordances see no kebab on the card.
### Topology view
Switch the segmented control to **Topology** to swap the grid for a hub-and-spoke graph. The local node sits in the centre with a cyan ring and the remotes radiate outward; the layout is computed automatically from the registered fleet.
<Frame>
<img src="/images/fleet-view/fleet-topology.png" alt="Fleet Topology view with the Local node card boxed in cyan in the centre showing 'CRITICAL · LOCAL' status pills, 16 cores CPU bar, RAM and DISK bars in destructive red, and '15 stacks · 17 running' below. Six remote node cards radiate to the right (SLX-Mars, sencho-test-03, sencho-test-02, sencho-test-01, sencho-pilot-test, Pitt-Moba, Opsix) each showing their CPU/MEM/DISK bars and stack summary. Cyan connector lines run from the local node to each remote. ReactFlow zoom controls sit at the bottom-left and the navigator minimap is pinned to the bottom-right." />
<img src="/images/fleet-view/fleet-topology.png" alt="Fleet Topology view in Hub layout. The Local node card sits at the centre with an Online pill, a 'prod' label pill, CPU / MEM / DISK bars, and '15 stacks · 16 running' below. Three remote cards radiate to the right (SLX-Mars, Pitt-Moba, Opsix), each with an Online pill, a 'dev' label pill where assigned, CPU/MEM/DISK bars, a stack/running summary, and a millisecond latency reading. Cyan connector lines run from the local node to each remote. ReactFlow zoom controls sit at the bottom-left and the navigator minimap is pinned to the bottom-right." />
</Frame>
Each node card in the topology graph carries:
- A **status pill** at the top reading `Online`, `Critical`, or `Offline`, with a coloured dot.
- A **Local** or **Remote** label in the top-right.
- A **lightning icon** when the node is critical; the card frame mutes when the node is offline.
- A **lightning icon** when the node is critical; a clock icon when a pilot-tunnel node's heartbeat has gone stale; the card frame mutes when the node is offline.
- The node name with a server icon.
- A row of **node label** pills, when the node carries any (matching the labels set in **Settings · Infrastructure · Nodes**), with a `+N` overflow tooltip past the first few.
- Three compact **CPU / MEM / DISK** bars with the percent value at the right of each row.
- A footer line summarising stacks and running containers, e.g. `3 stacks · 4 running`.
- A footer line summarising stacks and running containers, e.g. `3 stacks · 4 running`, plus a round-trip latency in milliseconds for online remotes.
**Connector lines** colour by the link's health: cyan when both ends are online, warning when the remote is critical, dashed and muted when the remote is offline.
@@ -151,7 +159,7 @@ Assign labels in **Settings · Infrastructure · Nodes** to drive Grouped mode.
Click **Stack details** on any online node card to expand the stack list. The right side of the row carries a `<n> stacks` counter once the list has loaded.
<Frame>
<img src="/images/fleet-view/fleet-drill-down.png" alt="Fleet grid with the Opsix node card expanded. The node header shows 4 running, 0 stopped, 3 stacks, and CPU / RAM / Disk usage bars. Below, the Stack details list shows three stacks (saelix-app-ca, saelix-app-wa, saelix-db). saelix-app-ca and saelix-db are expanded, each showing one container row with a green running dot, the container name, a 'running' state badge, the image tag, and an 'Up 2 weeks' status string." />
<img src="/images/fleet-view/fleet-drill-down.png" alt="Fleet grid with the Opsix node card's Stack details expanded, showing three stacks (saelix-app-ca, saelix-app-wa, saelix-db) each with a fleet label dot. saelix-db is expanded to show one container row with a green running dot, the container name, a 'running' state badge, the image tag mariadb:10.11, and an 'Up 6 weeks' status string." />
</Frame>
Each stack row carries the stack name, an expand chevron, a `running / total` container count, and any **fleet label dots** colour-coded from the node's label palette (when labels are configured for the stack).
@@ -183,10 +191,10 @@ A subtle `Auto-refreshing every 30 seconds` line at the bottom of the page confi
The **Status** tab gives you a fleet-wide rollup of which automations and security features are configured on every node, without having to open each node's settings individually.
<Frame>
<img src="/images/fleet-view/fleet-status-tab.png" alt="Fleet Status tab with eight node cards in a three-column grid. The Local card is in the top-left and shows the full eight-row summary (Agents None, Alert rules None, Auto-heal None, Webhooks None, MFA Off, Scanning None, Backup Enabled, Crash detect On). Opsix, Pitt-Moba, SLX-Mars, sencho-test-01/02/03 each show a six-row summary that omits MFA and Backup. The sencho-pilot-test card is offline and shows the muted message 'Node is unreachable. Configuration unavailable.'" />
<img src="/images/fleet-view/fleet-status-tab.png" alt="Fleet Status tab with four node cards. The Local card shows the full eight-row summary (Agents 1 active, Alert rules None, Auto-heal 1/1, Webhooks None, MFA Off, Scanning None, Backup Enabled, Crash detect On). Opsix, Pitt-Moba, and SLX-Mars each show a seven-row summary that omits MFA and Backup but adds a Policy sync row reading 'In sync'." />
</Frame>
Online nodes render a two-column summary grid with up to eight rows:
Online nodes render a two-column summary grid with up to nine rows:
| Row | What it shows | Visibility |
|-----|---------------|------------|
@@ -198,6 +206,7 @@ Online nodes render a two-column summary grid with up to eight rows:
| **Scanning** | Active vulnerability-scan policies, formatted `<n> policy/policies` | Always (when not gated) |
| **Backup** | `Enabled` or `Disabled` | Local node only |
| **Crash detect** | `On` or `Off` | Always |
| **Policy sync** | `In sync`, or a `degraded` / `paused` pill with a tooltip naming the cause. See [Fleet Sync](/features/fleet-sync). | Remote nodes that have received at least one sync push |
Offline nodes show a muted card with the heading and the message `Node is unreachable. Configuration unavailable.` The Online or Offline indicator in the card header confirms reachability at the time of the last fetch.
@@ -239,10 +248,10 @@ The map is fleet-wide: the control instance gathers each node's view and merges
## Node Updates
Click **Check Updates** in the page header to open the **Node Updates** sheet. From here you can read every node's current Sencho version, see which nodes have an update available, and trigger updates one node at a time or across the whole fleet. The sheet has two tabs: **Nodes** (the node table and summary) and **Changelog** (release notes for the available Sencho version, fetched from the GitHub Releases page).
Click **Node Update** in the Overview tab's toolbar to open the **Node Updates** sheet. From here you can read every node's current Sencho version, see which nodes have an update available, and trigger updates one node at a time or across the whole fleet. The sheet has two tabs: **Nodes** (the node table and summary) and **Changelog** (release notes for the available Sencho version, fetched from the GitHub Releases page).
<Frame>
<img src="/images/fleet-view/fleet-node-updates.png" alt="Node updates sheet titled 'Node updates · 8 nodes · 1 update available'. A Recheck button and an 'Update all (1)' button sit in the header. Below them, four summary cards read '7 Up to date', '1 Available', '0 Updating', '0 Failed'. A 'Filter nodes…' search box sits above the node table, which has columns Node / Type / Current / Latest / Status. Seven rows show a green 'Up to date' badge; the sencho-pilot-test row reports Current 'unknown' and surfaces an Update button on the right. The footer reads 'LATEST VERSION v0.76.3'." />
<img src="/images/fleet-view/fleet-node-updates.png" alt="Node updates sheet titled 'Node updates · 4 nodes · 0 updates available'. A Recheck button sits in the header. Below it, four summary cards read '4 Up to date', '0 Available', '0 Updating', '0 Failed'. A 'Filter nodes…' search box sits above the node table, which has columns Node / Type / Current / Latest / Status; all four rows (Local, Opsix, Pitt-Moba, SLX-Mars) show a green 'Up to date' badge at v0.95.0. The footer reads 'LATEST VERSION v0.95.0'." />
</Frame>
### Header actions
@@ -292,7 +301,7 @@ When you click **Update** on a remote row, Sencho dispatches the update command
When you click **Update** on the local row, a confirmation dialog appears first ("Update local node"). Confirming kicks off the same pull-and-recreate flow on the local Sencho instance. Because the gateway is restarting itself, a full-screen reconnecting overlay takes over the browser tab. The overlay polls `/api/health` every 3 seconds and dismisses itself once the new gateway answers. If the gateway has not returned within 5 minutes the overlay switches to a *Taking longer than expected* state with a **Reload to check** button rather than waiting indefinitely. A large image pull can legitimately run past that window, so this state is not a failure on its own.
<Note>
Triggering updates (per-row and **Update all**), skipping versions, and unskipping versions all require the **admin** role. Viewer and operator roles can read update status, the changelog, and the skipping state but cannot change them.
**Recheck**, triggering updates (per-row and **Update all**), skipping versions, and unskipping versions all require the **admin** role. Viewer and operator roles can read update status, the changelog, and the skipping state but cannot change them or force a recheck.
</Note>
## How fleet data is fetched
+22 -16
View File
@@ -11,7 +11,7 @@ The **Logs** tab aggregates output from the containers Sencho manages into a sin
</Note>
<Frame>
<img src="/images/global-observability/global-observability-overview.png" alt="Global Observability page with the live logs masthead, four-tile signal rail, filter strip, and a feed body showing day-banded rows in info green and error rose." />
<img src="/images/global-observability/global-observability-overview.png" alt="Global Observability page with the 'Streaming' masthead, four-tile signal rail, filter strip, and a feed body showing a mix of info, warning, and error rows from several containers." />
</Frame>
## Status masthead
@@ -19,30 +19,30 @@ The **Logs** tab aggregates output from the containers Sencho manages into a sin
The masthead at the top of the page is the single place to read the stream's current condition.
<Frame>
<img src="/images/global-observability/masthead.png" alt="Logs masthead with the kicker 'LIVE LOGS · NODE · LOCAL', the state word 'Streaming' in editorial italics next to a pulsing brand-cyan dot, and the meta tiles 'LAST EVENT NOW' and 'SESSION 1M 43S' on the right." />
<img src="/images/global-observability/masthead.png" alt="Logs masthead with the kicker 'LIVE LOGS · NODE · LOCAL', the state word 'Idle' in editorial display type against a steady gray left edge rail, and the meta tiles 'LAST EVENT NOW' and 'SESSION 0M 10S' on the right." />
</Frame>
It carries:
- A **kicker** in uppercase mono tracking that reads `LIVE LOGS · NODE · LOCAL`, a reminder that the feed is the hub's own managed containers.
- A **pulsing dot** that mirrors the stream tone: brand cyan and pulsing while events arrive within the live window, gray and steady when the stream goes quiet, solid rose when the underlying SSE connection has errored.
- A **state word** set in the editorial display face: `Streaming` while the dot is live, `Idle` when the stream has gone quiet, `Offline` when the connection has failed.
- A **left edge rail** that mirrors the stream tone: brand cyan and shimmering while events arrive within the live window, gray and steady when the stream goes quiet, solid rose when the underlying SSE connection has errored.
- A **state word** set in the editorial display face, tinted to match the rail: `Streaming` while the rail shimmers cyan, `Idle` when the stream has gone quiet, `Offline` when the connection has failed.
- A **LAST EVENT** stat that names the band of the most recent event using the same vocabulary as the feed (`NOW`, `2M AGO`, `1H AGO`, then the calendar date), so you can see how stale the latest line is at a glance.
- A **SESSION** stat counting how long this Logs tab has been open, formatted with uppercase letter suffixes: `1H 43M` once the session crosses an hour, `0M 12S` under it.
The state word flips to `Idle` after ten seconds without an event. That is a feature of the readout, not the stream: the SSE connection stays open in the background and the dot will pulse cyan again on the next event.
The state word flips to `Idle` after ten seconds without an event. That is a feature of the readout, not the stream: the SSE connection stays open in the background and the rail will shimmer cyan again on the next event.
## Signal rail
A four-tile rail directly under the masthead summarizes the current buffer.
<Frame>
<img src="/images/global-observability/signal-rail.png" alt="Four-tile signal rail: 'EVENTS / MIN' showing 20 with a 60-second sparkline, 'ERRORS' showing 418 in destructive red, 'WARNINGS' showing 9 in amber, 'CONTAINERS' showing 8 in stat-value white." />
<img src="/images/global-observability/signal-rail.png" alt="Four-tile signal rail: 'EVENTS / MIN' showing 5 with a sparkline beside the number, 'ERRORS' showing 247 in destructive red, 'WARNINGS' showing 17 in amber, 'CONTAINERS' showing 11 in stat-value white." />
</Frame>
| Tile | What it shows |
|------|---------------|
| **EVENTS / MIN** | Total events received in the last sixty seconds, with a 60-bucket sparkline (one bucket per second) under the number. The sparkline shifts left every tick so you read it as a live rhythm rather than a static chart. |
| **EVENTS / MIN** | Total events received in the last sixty seconds, with a 60-bucket sparkline (one bucket per second) beside the number. The sparkline shifts left every tick so you read it as a live rhythm rather than a static chart. |
| **ERRORS** | Count of `ERROR`-level lines in the buffer. Tints destructive red whenever the count is above zero; otherwise the number sits in subtitle gray. |
| **WARNINGS** | Count of `WARN`-level lines in the buffer. Tints warning amber whenever the count is above zero; otherwise the number sits in subtitle gray. |
| **CONTAINERS** | Count of distinct container names present in the in-memory buffer (after the **Clear** cutoff). Containers whose last entry has aged out of the buffer drop out of the count. |
@@ -71,14 +71,14 @@ Filters AND together and run in the browser, so there is no per-keystroke round-
The body of the page is a chronological list of every log line in the buffer that passes the current filter.
<Frame>
<img src="/images/global-observability/feed-bands.png" alt="Feed body showing day band headers '3M AGO' and '2M AGO' between groups of rows. Each row shows a green INFO dot, a 24-hour timestamp, a cyan container name (seerr, profilarr), and the raw log message." />
<img src="/images/global-observability/feed-bands.png" alt="Feed body showing the '1H AGO' band header between groups of rows, all from the radarr container. Info rows show a green dot and no row tint; one row shows a warning-amber dot with the row's whole background tinted amber." />
</Frame>
Each row carries:
- A small **severity dot** tinted by detected level: success green for `INFO`, warning amber for `WARN`, destructive rose with a soft glow for `ERROR`.
- A **timestamp** in 24-hour local time (`HH:MM:SS`), set in tabular nums so columns line up across rows.
- A **container name** in brand cyan. Hover to see the full `<stack>/<container>` path in a tooltip; the displayed name is the compose service name with the project prefix and replica suffix stripped.
- A **container name** in brand cyan. Hover to see the full `<stack>/<container>` path in a tooltip; the displayed name is the compose service name with the project prefix stripped, and the `-1` / `_1` replica suffix removed for a service's first replica. A service scaled beyond one replica keeps its numeric suffix (`worker-2`, `worker-3`) so you can tell the copies apart.
- A **message** containing the raw log output. `STDERR`-source lines render in destructive rose; `STDOUT`-source lines render in stat-value white.
A row's whole-row tint follows its detected level. Error rows pick up a subtle destructive wash, warning rows pick up an amber wash, info rows have no tint and gain a subtle accent on hover. The detected level is independent of the source, so an `STDOUT` line that contains the word `ERROR:` still gets the rose row tint.
@@ -96,7 +96,7 @@ The viewport autoscrolls to follow new rows. If you scroll up to read older entr
A floating control strip is anchored to the bottom-right of the feed.
<Frame>
<img src="/images/global-observability/paused-resume-chip.png" alt="Floating control strip with a brand-cyan '22 NEW · RESUME' pill on the left and the Pause/Clear/Download chip group on the right. The Pause button shows a play icon and the label 'RESUME', indicating the stream is currently paused." />
<img src="/images/global-observability/paused-resume-chip.png" alt="Floating control strip with a brand-cyan '29 NEW · RESUME' pill on the left and the Resume/Clear/Download chip group on the right. The toggle shows a play icon and the label 'RESUME', indicating the stream is currently paused." />
</Frame>
| Action | What it does |
@@ -110,10 +110,10 @@ A floating control strip is anchored to the bottom-right of the feed.
The **Level** segmented control drops the feed to a single severity. Pairing it with **Stacks** is a fast way to investigate a misbehaving stack.
<Frame>
<img src="/images/global-observability/error-only-filter.png" alt="Logs page with the Level filter set to 'Error'. The feed shows only error-tinted rows from the sencho and radarr containers, with day bands '6M AGO' through 'NOW' visible." />
<img src="/images/global-observability/error-only-filter.png" alt="Logs page with Stacks narrowed to radarr and the Level filter set to Error. The feed shows only the two matching error-tinted rows, under a '3H AGO' band header." />
</Frame>
The level on each row is detected from the message body, not the stream source. Sencho's parser scans the line for keywords (`info`, `debug`, `warn`, `error`) and structured fields (`level=...`, `[INFO]`, `Exception:`) before falling back to a source-based default of `STDERR → ERROR`, `STDOUT → INFO`. That means an `STDOUT` line containing `ERROR:` classifies as `ERROR` (and gets the rose tint), and an `STDERR` line containing `info` classifies as `INFO` (and gets the green dot).
The level on each row is detected from the message body, not the stream source. Sencho's parser checks three tiers in order and stops at the first match: first `info` / `debug` / `trace` keywords or structured fields (`level=info`, `[INFO]`), then `warn` / `warning`, then `error` / `fatal` / `crit` / `critical` / `panic` or a leading `Exception:`. Only when none of those match does it fall back to a source-based default of `STDERR → ERROR`, `STDOUT → INFO`. The INFO tier is checked first, so it wins even on an `STDERR` line: an `STDERR` line containing `info` classifies as `INFO` (and gets the green dot), while an `STDOUT` line containing `ERROR:` falls through every tier to the ERROR match (and gets the rose tint).
## How streaming works
@@ -129,7 +129,7 @@ Three budgets keep the page responsive even on a chatty host:
- **Buffer** of two thousand entries in the browser's memory. As new events arrive past the cap, the oldest entries are dropped. The buffer keeps filling while the feed is paused, so a long pause does not stretch memory unbounded.
- **Rendered rows** capped at three hundred. Beyond that, the `Showing last 300 of <n>` banner appears so you know more matches exist behind the slice.
- **Followed containers** capped per stream. When the hub manages more containers than the cap, the feed follows the first set and posts a `Following <n> of <m> managed containers` notice so you know the feed is truncated; use the per-container log viewer for the rest.
- **Followed containers** capped at sixty per stream. When the hub manages more than sixty containers, the feed follows the first sixty and posts a `Following <n> of <m> managed containers; <count> not shown` line into the feed itself (as a synthetic warning row from a `sencho` entry, not a passive banner) so you know the feed is truncated; use the per-container log viewer for the rest.
When you need to chase older lines than the buffer holds, jump to the [per-container log viewer](/features/editor#log-viewer) in the Editor tab or run `docker compose logs` from the [Host Console](/features/host-console).
@@ -160,8 +160,8 @@ The page is built from several independent loops so the masthead and the sparkli
<Accordion title="The masthead reads 'Offline' and a 'Failed to fetch logs. Retrying...' banner is visible">
The polling fallback got an HTTP error when it tried to read `/api/logs/global`. Check that the host's Docker daemon is reachable on the socket Sencho is configured to use. The page retries the request every five seconds; once it succeeds, the banner clears on its own.
</Accordion>
<Accordion title="The dot is gray and the state reads 'Idle' even though logs are appearing further down the page">
`Idle` only checks whether an event has arrived in the last ten seconds. If your fleet is genuinely quiet (a maintenance window, a single low-traffic container), the dot rests gray and the band headers in the feed roll up to `2M AGO`, `5M AGO`, and so on. The next event flips the dot back to brand cyan and the state word back to `Streaming` on the next one-second tick.
<Accordion title="The masthead rail is gray and the state reads 'Idle' even though logs are appearing further down the page">
`Idle` only checks whether an event has arrived in the last ten seconds. If your fleet is genuinely quiet (a maintenance window, a single low-traffic container), the rail rests gray and the band headers in the feed roll up to `2M AGO`, `5M AGO`, and so on. The next event flips the rail back to a shimmering brand cyan and the state word back to `Streaming` on the next one-second tick.
</Accordion>
<Accordion title="A line that says 'ERROR' renders without the rose row tint">
Severity is detected once per line by scanning the message body. Some loggers print `ERROR` as part of a stack trace header on a line that does not itself match the parser's keyword set, in which case the line is classified by its source: `STDOUT` falls back to `INFO`. To force the row to your preferred level, change the application's log format to something the parser recognises (`level=error`, `[ERROR]`, or a leading `Error:` / `Exception:`). The behavior is intentional: it keeps a noisy `STDOUT` line that quotes the word `ERROR` from polluting the error count.
@@ -172,8 +172,14 @@ The page is built from several independent loops so the masthead and the sparkli
<Accordion title="Clear hid the rows but a few old lines came back a moment later">
**Clear** records the click time and hides every row whose timestamp is earlier than that moment, but the SSE stream keeps delivering events whose timestamps were generated by Docker before the click. A line emitted at `16:51:43.987` that arrives at the browser at `16:51:44.020` will still pass the filter even if you clicked **Clear** at `16:51:44.000`. The lag is normally a few hundred milliseconds; click **Clear** again to hide them.
</Accordion>
<Accordion title="A line shows stray fragments like [36m or [0m in the message">
Some applications color their console output with ANSI escape sequences. Sencho strips the non-printable escape character so it cannot corrupt the terminal-style rendering, but the printable characters that followed it (the color code and the closing reset, e.g. `[36m` or `[0m`) are left in the message text. This is cosmetic only; it does not affect level detection or search. If the noise bothers you, disable colored logging in the source application (most loggers respect `NO_COLOR` or a `--no-color` flag) or configure it to log JSON instead.
</Accordion>
<Accordion title="A scaled service's container name still shows a number, like worker-2">
The replica-suffix strip only removes a trailing `-1` or `_1`, the suffix Docker Compose assigns to a service's first replica. Additional replicas (`-2`, `-3`, and so on) keep their numeric suffix so you can tell the copies apart in the feed and in the Stacks filter.
</Accordion>
<Accordion title="A container I expect is missing from the feed">
The feed shows the containers Sencho manages (those started from its compose directory). A container started outside Sencho, or one belonging to a stack on a different node, does not appear. Open it from the [per-container log viewer](/features/editor#log-viewer) in the Editor, or use the [Host Console](/features/host-console) for ad-hoc `docker compose logs`.
The feed shows the containers Sencho manages (those started from its compose directory). A container started outside Sencho, or one belonging to a stack on a different node, does not appear. If the hub manages more than sixty containers, only the first sixty are followed; check for the `Following <n> of <m> managed containers` row. Either way, open the missing container from the [per-container log viewer](/features/editor#log-viewer) in the Editor, or use the [Host Console](/features/host-console) for ad-hoc `docker compose logs`.
</Accordion>
<Accordion title="A row says a log stream ended unexpectedly">
When one container's follow stream drops (the container was removed, or the Docker daemon hiccuped), the feed posts a single warning row for that container and stops following it; the other containers keep streaming. Reopen the Logs tab to re-attach to every managed container.
+26 -11
View File
@@ -6,18 +6,19 @@ description: Jump to any page, node, or stack from anywhere in the app with a si
The **global search palette** lets you move around Sencho without reaching for the mouse. It covers the reachable page destinations for your tier and role (the same page list Classic top navigation, Smart primary and More, and mobile navigation use), every configured node, and every stack on every online node in your fleet. Compact launcher can also open **Settings** from its menu; Settings is not a palette page row.
<Frame>
<img src="/images/global-search/palette-pages.png" alt="Sencho global search palette open with no query, the Pages group listing Home, Fleet, Resources, Networking, App Store, Logs, and Auto-Update each with a leading icon." />
<img src="/images/global-search/palette-pages.png" alt="Sencho global search palette open with no query over the blurred dashboard, the Pages group listing Home, Fleet, Resources, Networking, Security, and App Store each with a leading icon, with a scrollbar indicating more entries below." />
</Frame>
## Opening the palette
Three ways to open it:
Two ways to open it:
- Press <kbd>Ctrl</kbd> + <kbd>K</kbd> (or <kbd>Cmd</kbd> + <kbd>K</kbd> on macOS) from anywhere in the app.
- Click the search icon in the top bar, left of the notification bell.
- Press <kbd>Esc</kbd> to close it at any time.
- Click the search icon in the top bar, left of the theme switch and notification bell. On a phone, the top bar is replaced by the **More** menu; see [On a phone](#on-a-phone) below.
The shortcut works from the dashboard, editor, fleet view, resources, and every other screen.
Press <kbd>Esc</kbd> to close it at any time, or click outside the dialog.
The shortcut works from the dashboard, editor, fleet view, resources, and every other screen, as long as focus isn't inside a text field. See the Troubleshooting section below if the shortcut seems unresponsive.
## What you can find
@@ -29,10 +30,10 @@ The palette groups results into three sections.
| **Nodes** | Every node in your fleet, with a green dot for online and a grey dot for offline. The currently active node carries a small **ACTIVE** chip on the right. | Switches the active node without leaving the current page |
| **Stacks** | Every compose stack on every online node, matched on the compose filename (extension included). | Switches to the stack's node and opens it in the editor |
Each stack row shows a status dot on the left (green for running, grey for exited, faded grey for unknown) and the node name as a mono tag on the right. That way you always know which host a match lives on before you pick it.
Each stack row shows a status dot on the left (green for running, grey for exited, faded grey for unknown, amber for partial) and the node name as a mono tag on the right. Partial means some of the stack's containers are up and some are not, the same state the sidebar shows as `PT`. That way you always know which host a match lives on, and roughly what state it's in, before you pick it.
<Frame>
<img src="/images/global-search/palette-nodes.png" alt="Global search palette scrolled to the Nodes group, with the active node showing a small ACTIVE chip on the right and a green online dot to the left of each node name." />
<img src="/images/global-search/palette-nodes.png" alt="Global search palette scrolled to the Nodes group, listing four nodes each with a green online dot, and the active Local node showing a small ACTIVE chip on the right." />
</Frame>
## Typing to filter
@@ -44,7 +45,7 @@ Start typing and the results narrow in real time. Pages and nodes filter instant
- `db` lists every stack with "db" in its filename, showing which node each one lives on.
<Frame>
<img src="/images/global-search/palette-stacks.png" alt="Global search palette with the query 'saelix' producing three Stacks rows, each with a green status dot on the left and a mono OPSIX node tag on the right." />
<img src="/images/global-search/palette-stacks.png" alt="Global search palette with the query 'db' producing two Stacks rows on different nodes: saelix-db with a green status dot and an OPSIX node tag, and db-compose with a grey status dot and an SLX-MARS node tag." />
</Frame>
When a query matches stacks on a remote node, selecting one switches the active node and opens the editor on that stack in a single action.
@@ -69,6 +70,20 @@ Once the palette is open:
Offline nodes show as greyed-out and are not selectable.
## On a phone
The top bar that hosts the search icon is dropped on a phone; every screen leads with its own masthead instead. Open the **More** menu (the menu icon in the top-right corner of any mobile screen) and tap **Search** at the top of the list to open the same palette.
<Frame>
<img src="/images/global-search/mobile-more-menu.png" alt="Sencho mobile More menu open on a phone, with Search listed above the full set of navigation destinations." />
</Frame>
The palette itself renders as a full-width sheet instead of a centered dialog, but the Pages, Nodes, and Stacks groups, the debounced fleet-wide stack search, and the keyboard-free tap-to-select behavior all work the same as on desktop.
<Frame>
<img src="/images/global-search/mobile-palette.png" alt="Global search palette open on a phone, showing the Pages group as a full-width list." />
</Frame>
## Troubleshooting
<AccordionGroup>
@@ -81,10 +96,10 @@ Offline nodes show as greyed-out and are not selectable.
<Accordion title="A stack I just created on another node does not show up">
The fleet-wide stack list is gathered once when you begin a search and reused while you keep typing, so a stack created on a remote node after you started typing will not appear yet. Clear the query and search again to pull a fresh list from the fleet.
</Accordion>
<Accordion title="A stack row shows a faded grey status dot">
The dot reflects the stack's run state: green for running, grey for exited, faded grey for unknown. Unknown means the node listed the stack but its status could not be read at that moment, usually a transient hiccup talking to that host's Docker engine. The stack is still selectable; open it to see its real state in the editor.
<Accordion title="A stack row shows a faded grey or amber status dot">
The dot reflects the stack's run state: green for running, grey for exited, faded grey for unknown, amber for partial. Partial means some of the stack's containers are up and some are not. Unknown means the node listed the stack but its status could not be read at that moment, usually a transient hiccup talking to that host's Docker engine. The stack is still selectable; open it to see its real state in the editor.
</Accordion>
<Accordion title="Ctrl+K does not open the palette">
The shortcut is handled at the window level, so it works from any screen, but a focused element that captures the keystroke first (for example a terminal in the host console) can intercept it. Click outside that element, or use the search icon in the top bar to open the palette instead.
The shortcut is handled at the window level, so it works from any screen, but it deliberately does nothing while focus is inside a text field: a text input, a text area, or the compose editor. This avoids hijacking a keystroke you're using to type (on macOS, Ctrl+K is also the native "delete to end of line" shortcut inside a field). Click outside the field first, or use the search icon in the top bar to open the palette instead.
</Accordion>
</AccordionGroup>
+2 -1
View File
@@ -27,6 +27,7 @@ The verdict is computed from signals Sencho already tracks, so the check is fast
- **Current containers**: a container that is already unhealthy, restarting, or crashed before the update makes the result hard to evaluate; the dialog asks you to look first.
- **Healthcheck coverage**: how many services define healthchecks, which determines how thoroughly the post-update health gate can verify the result.
- **Pending update**: the [update preview](/features/auto-update-policies) classifies the pending change. A major version bump asks for review; patch updates and same-tag refreshes pass quietly.
- **Local build services**: any service built from a local Dockerfile instead of pulled from a registry. The update rebuilds these from source, which depends on the local Dockerfile context and can take longer than a plain pull; this signal never blocks the verdict.
- **Rollback backup**: whether a backup slot exists and how old it is. A missing backup is only a note, because the update itself creates a fresh one when it starts.
- **Node disk**: disk usage near or above the node's alert threshold warns you before a large pull fills the disk.
@@ -39,7 +40,7 @@ On stacks with more than one Compose service, you can also open readiness for a
When the selected service is build-backed and has no registry update, the proceed button reads **Rebuild** instead of **Update**. A per-service update badge appears only when a registry update is confirmed for that service.
<Frame>
<img src="/images/health-gated-updates/readiness-dialog.png" alt="Update readiness dialog showing the verdict chip, signal rows for Compose Doctor, Drift, Current containers, Healthcheck coverage, Pending update, Rollback backup and Node disk, the fleet snapshot row with its checkbox, and the Cancel and Update now buttons" />
<img src="/images/health-gated-updates/readiness-dialog.png" alt="Update readiness dialog with a review required verdict, showing signal rows for Compose Doctor, Drift, Current containers, Healthcheck coverage, Pending update, Local build services, Rollback backup and Node disk, the fleet snapshot row, and the Cancel and Update now buttons" />
</Frame>
## The post-update health gate
+56 -36
View File
@@ -12,12 +12,12 @@ Sencho is [AGPLv3](https://github.com/studio-saelix/sencho/blob/main/LICENSE) op
## Plans
| Tier | Annual (per mo, billed yearly) | Monthly | Seats |
|------|--------------------------------|---------|-------|
| **Community** | Free | Free | Unlimited |
| **Admiral** | $8.25 | $12 | Unlimited |
| Tier | Price | Seats |
|------|-------|-------|
| **Community** | Free forever | Unlimited |
| **Admiral** | Business assurance, arranged with Studio Saelix | Unlimited |
See [the pricing page](https://sencho.io/pricing) for current pricing.
Admiral is arranged directly with Studio Saelix rather than a self-serve checkout. See [the pricing page](https://sencho.io/pricing) for the current feature comparison, or email **[hello@sencho.io](mailto:hello@sencho.io)** to start a conversation.
### Feature breakdown
@@ -49,7 +49,7 @@ See [the pricing page](https://sencho.io/pricing) for current pricing.
**Admiral** is the official business assurance plan from Studio Saelix. It includes everything in Community, plus:
- **Hardened Build:** an Admiral image channel with published supply-chain assurance artifacts
- **Hardened Build:** an Admiral image channel with published supply-chain assurance artifacts, switched from **Settings → Admiral Account**; see [Switching to Hardened Build](#switching-to-hardened-build) below
- **Managed continuity:** Recovery Vault (a managed, off-site snapshot allowance)
- **Assurance and support:** priority email support and Studio Saelix-backed continuity for production fleets
- **Governance:** advanced RBAC roles (Deployer, Node Admin, Auditor), scoped permissions per stack or node, and audit log export (CSV, JSON), anomaly detection, and configurable retention beyond the recent window
@@ -58,20 +58,11 @@ See [the pricing page](https://sencho.io/pricing) for current pricing.
**Planned assurance services** (not available today; no delivery date committed): Release Safety Channel, Production Assurance Reports, and Fleet Beacon.
## Free trial
## Getting an Admiral license
Sencho offers a **14-day Admiral trial** so you can evaluate Admiral assurance (priority support path, Recovery Vault, Hardened Build, governance depth including advanced RBAC, LDAP / Active Directory, and audit export) with your real infrastructure before committing. The trial is offered on the monthly and annual Admiral plans.
Admiral is arranged directly with Studio Saelix rather than through a self-serve checkout. Email **[hello@sencho.io](mailto:hello@sencho.io)**, or click **Get in touch** on [the pricing page](https://sencho.io/pricing), to talk through pricing and a trial period for your fleet. However you receive it, your license key arrives by email and activates the same way, described next in [Activating your license](#activating-your-license).
To start a trial:
1. Visit the [pricing page](https://sencho.io/pricing).
2. Switch to the **Annual** or **Monthly** billing tab.
3. Click **Start 14-day trial** on the **Admiral** card.
4. Complete the checkout. A valid card is required for verification; you are not charged until the trial ends.
5. Your license key is emailed to you within a few minutes.
6. Activate the key in the Sencho dashboard as described in [Activating your license](#activating-your-license).
When your trial ends, the card you provided is automatically charged and your plan continues as a paid Admiral subscription. To avoid being charged, cancel from the **Manage subscription** button in **Settings → Admiral Account** (or from your receipt email) any time before day 14.
A trial license activates and behaves exactly like a full Admiral license: every Admiral feature is unlocked immediately, and the only difference is the countdown shown in [The Plan section](#the-plan-section) below. What happens when the trial ends is part of your agreement with Studio Saelix; check your activation email or ask your sales contact.
<Tip>
Fresh installs land on the **Community** tier until you activate a license key. All Community features (unlimited nodes, compose editor, global logs, alerts, Custom OIDC, and more) are available immediately.
@@ -82,7 +73,7 @@ When your trial ends, the card you provided is automatically charged and your pl
Open **Settings → Admiral Account** in the Sencho dashboard. When the license is not active, the **Activate** section sits below the Plan card:
<Frame>
<img src="/images/licensing/license-activate-section.png" alt="The Activate section of the License page on a Community-tier instance, with a License key input field and an Activate button on the right" />
<img src="/images/licensing/license-activate-section.png" alt="The Activate section of the Admiral Account page on a Community-tier instance, with a License key input field and an Activate button on the right" />
</Frame>
1. Paste your key into the **License key** field.
@@ -92,38 +83,64 @@ Sencho validates the key and activates your plan. If activation fails, the toast
## The Plan section
When a license is active, **Settings → Admiral Account** opens on the **Plan** section. The page masthead at the top exposes three stat pills, and the section below lists the metadata for the active license.
**Settings → Admiral Account** opens on the **Plan** section. The page masthead exposes stat pills that vary by license state, and the section below lists your plan details and current image channel.
<Frame>
<img src="/images/licensing/license-admiral-active.png" alt="License page showing an active Sencho Admiral license, with the Plan section listing the Customer, Product, and masked License key fields and a Deactivate button" />
<img src="/images/licensing/license-admiral-active.png" alt="Admiral Account page showing an active lifetime Sencho Admiral license, with Plan, Recovery Vault, Hardened Build, Current image, and Channel rows, plus Customer, Product, and masked License key fields and a Deactivate button" />
</Frame>
The masthead pills are:
The masthead pills:
| Pill | Meaning |
|------|---------|
| **SCOPE** | Reads `operator` when you are signed in as an admin. |
| **SCOPE** | Always `global`: a license applies to the whole control plane, not a single node. |
| **PLAN** | The current tier: `community` or `admiral`. Trial licenses show `admiral`. |
| **RENEWS** / **TRIAL** / **STATUS** | `RENEWS: <date>` for active subscriptions, `TRIAL: Xd left` for trials, and `STATUS: expired` for expired licenses. |
| **RENEWS** | Shown for an active, non-lifetime subscription with a renewal date. |
| **DURATION** | Shown as `lifetime` in place of RENEWS for a legacy license with no expiry. |
| **TRIAL** | `Xd left` while a trial is active. |
| **STATUS** | `expired` for an expired license. |
Community and disabled licenses show only the SCOPE and PLAN pills.
The **Plan** card lists:
- **Tier name** (e.g. `Sencho Admiral`) with a short status line, such as "Active license on this control plane", "Trial: X days remaining", or "Your license has expired."
- **Customer**: the customer name on the purchase.
- **Product**: the purchased product (e.g. `Sencho Admiral`).
- **License key**: the last four characters of your key, displayed as `****-****-****-XXXX`. The full key is never re-displayed after activation.
- **Tier name** (`Sencho Community` or `Sencho Admiral`) with a status line, such as "Community plan. Full AGPLv3 self-hosted control plane.", "Active license on this control plane.", "Trial: X days remaining.", "Your Admiral license has expired...", or "Your license has been disabled...".
- **License** (Community only): a link to view the AGPLv3 source on GitHub.
- **Recovery Vault** (Admiral only): confirms the entitlement is included in your subscription.
- **Hardened Build** (Admiral only): a **Switch to Hardened** button; see [Switching to Hardened Build](#switching-to-hardened-build) below.
- **Current image** and **Channel**: the image reference and channel (`Community` or `Hardened`) this control plane is currently running.
- **Customer**, **Product**, and **License key** (active licenses only): purchase metadata and your key masked to its last four characters (`****-****-****-XXXX`). The full key is never re-displayed after activation.
## Switching to Hardened Build
Hardened Build is a separate, entitlement-gated image channel available to Admiral instances: a Studio Saelix-published image with supply-chain assurance artifacts, distinct from the default Community image. Access is checked live against Studio Saelix's assurance service every time you use it, not just against your Admiral tier, so an Admiral license alone does not guarantee Hardened Build is enabled for your account.
To switch:
1. Open **Settings → Admiral Account** from an interactive admin session. API tokens cannot manage image channels.
2. Click **Switch to Hardened** in the Plan card.
3. Sencho checks your entitlement and registry access, then opens a review dialog listing the **current** image, the **target** Hardened image, your **registry** access status, the **compose path** that will be edited, and the **pin kind** (tag or digest). If the entitlement check fails, you'll see "Hardened image access is unavailable"; contact Studio Saelix.
4. Confirm the switch. Sencho pulls the Hardened image using credentials from [Registries](/features/private-registries), rewrites your compose file, and recreates the service. This restarts Sencho.
Back up first (a fleet snapshot or a copy of your compose file) since this rewrites your compose file and restarts the control plane. If the switch fails partway through, the Plan card shows an **Image operation** field with the failure reason and an **Acknowledge failure** button, which clears the failed state so you can retry.
<Note>
Once a node is on the Hardened channel, only an interactive admin session on that node can pull further Hardened updates. Fleet's remote **Update all** and per-node **Update** actions cannot update a Hardened node from elsewhere in the fleet; update it directly on that node's own dashboard instead.
</Note>
## Managing your subscription
When the license is an active subscription, the Plan section action row exposes **Manage subscription** alongside **Deactivate**. **Manage subscription** opens the billing portal in a new tab, where you can update your payment method, view invoices, cancel, or switch plans.
For an active, non-lifetime subscription, the Plan section's action row exposes **Manage subscription** alongside **Deactivate**. **Manage subscription** opens the Lemon Squeezy billing portal in a new tab, where you can update your payment method, view invoices, cancel, or switch plans.
The same portal is reachable from the profile menu in the top-right corner of the app:
<Frame>
<img src="/images/licensing/profile-menu.png" alt="Profile dropdown popover showing an identity header with the admin role and Admiral tier badges, a navigation strip with Settings, Documentation, and Feedback entries, an Appearance theme picker, and a Log Out button" />
<img src="/images/licensing/profile-menu.png" alt="Profile dropdown popover showing the avatar initials, an Admin role badge, and an Admiral tier badge, followed by Settings, Documentation, and Open New Issue links and a Log Out button" />
</Frame>
Click your initials in the top-right corner to open the popover. For active subscription licenses, a **Billing** row appears between **Settings** and **Documentation** that opens the same portal as **Manage subscription**.
Click your initials in the top-right corner to open the popover. For an active, non-lifetime subscription, a **Billing** row appears between **Settings** and **Documentation** that opens the same portal as **Manage subscription**.
Legacy lifetime licenses show only **Deactivate** in the Plan card, no **Manage subscription** button, and no **Billing** row in the profile menu, since there is no recurring subscription for Lemon Squeezy to manage.
## License validation
@@ -131,20 +148,23 @@ Active licenses are re-validated every **72 hours**. If your instance goes offli
## License states
The License page renders differently depending on the license status:
The Admiral Account page renders differently depending on the license status:
| Status | Plan section | Activate section | Pricing section |
|--------|--------------|------------------|-----------------|
| **Community** | `Sencho Community` with "Community plan. Full AGPLv3 self-hosted control plane." | Visible | Visible |
| **Trial** | `Sencho Admiral (Trial)` with a countdown chip | Visible (so a paid key can replace the trial) | Hidden |
| **Active subscription** | Tier name with Customer, Product, License key, plus `Manage subscription` and `Deactivate` | Hidden | Hidden |
| **Trial** | `Sencho Admiral (Trial)` with a countdown chip | Visible (so a full key can replace the trial) | Hidden |
| **Active subscription** | Tier name with Customer, Product, License key, **Manage subscription**, and **Deactivate** | Hidden | Hidden |
| **Active, lifetime** | Same as an active subscription, but no **Manage subscription** button since there's no subscription to manage | Hidden | Hidden |
| **Expired** | `Sencho Community` with "Your Admiral license has expired. Renew to restore Admiral assurance (priority support, Recovery Vault, Hardened Build, and governance)." and a destructive **Status: Expired** field | Visible | Visible |
| **Disabled** | `Sencho Community` with "Your license has been disabled. Contact support for assistance." | Visible | Visible |
The **Pricing** section, when visible, holds a single **See pricing** button that opens [the pricing page](https://sencho.io/pricing) in a new tab so you can pick a tier and billing cadence.
The **Recovery Vault** and **Hardened Build** rows in the Plan card only appear once a license resolves to Admiral (active, trial, or lifetime); Community, expired, and disabled states never show them.
The **Pricing** section, when visible, holds a single **See pricing** button that opens [the pricing page](https://sencho.io/pricing) in a new tab.
<Frame>
<img src="/images/licensing/license-community.png" alt="License page on a Community-tier instance, with the masthead showing SCOPE operator and PLAN community, then the Plan section reading 'Sencho Community · Community plan. Full AGPLv3 self-hosted control plane.', the Activate section with the License key input, and the Pricing section with the See pricing button" />
<img src="/images/licensing/license-community.png" alt="Admiral Account page on a Community-tier instance, with the masthead showing SCOPE global and PLAN community, the Plan section reading 'Sencho Community · Community plan. Full AGPLv3 self-hosted control plane.' with a View source link, the Activate section with the License key input, and the Pricing section with the See pricing button" />
</Frame>
## Multi-node license enforcement
+29 -24
View File
@@ -7,7 +7,7 @@ description: Connect multiple Sencho instances and manage every server from one
Sencho's multi-node feature lets you operate Docker Compose stacks on every server you run from a single browser tab. Each server runs its own Sencho instance and the control instance acts as a transparent proxy to the others.
<Frame>
<img src="/images/multi-node/node-manager.png" alt="Settings · Infrastructure · Nodes panel showing the masthead breadcrumb, SCOPE/NODES/REMOTE stat strip, Add node button, Generate Node Token card, and a populated table of one Local row plus seven remote rows with mixed Proxy and Pilot Agent modes." />
<img src="/images/multi-node/node-manager.png" alt="Settings · Infrastructure · Nodes panel showing the masthead breadcrumb, SCOPE/NODES/REMOTE stat strip, Add node button, Generate Node Token card, and a populated table of one Local row plus three Proxy-mode remote rows." />
</Frame>
## How it works
@@ -30,19 +30,19 @@ If multiple local nodes exist from an older version, the extra rows show a Delet
When you add a remote node, the Mode picker offers two options. The form tells you the difference inline:
> Pilot Agent requires only outbound HTTPS from the remote host. Distributed API Proxy requires the remote host to expose an inbound port.
> Pilot Agent requires only outbound HTTPS from the remote host. Distributed API Proxy requires the remote host to expose an inbound port. Sencho Mesh works with both modes.
| | Pilot Agent (default) | Distributed API Proxy |
|---|---|---|
| **Direction** | Remote dials the control plane (outbound only) | Control plane dials the remote (inbound to remote) |
| **Network requirement** | Outbound HTTPS to the control instance | Inbound TCP port reachable from the control instance |
| **What you run on the remote host** | One `docker run` command issued at enrollment | A full Sencho install reachable on a routable URL |
| **What you run on the remote host** | A generated Compose file, brought up with `docker compose up -d` | A full Sencho install reachable on a routable URL |
| **Token model** | One-shot enrollment token, 15-minute expiry | Long-lived bearer token, rotates on regeneration |
| **Best for** | Hosts behind NAT, dynamic IPs, restrictive ingress, edge boxes | Servers you already expose on a stable URL with TLS termination |
If you are not sure, start with **Pilot Agent**. It is the default because it works in more network shapes and the enrollment is a single command.
If you are not sure, start with **Pilot Agent**. It is the default because it works in more network shapes and the enrollment is a single Compose file.
For a deep look at how the pilot tunnel works under the hood (credential lifecycle, security model, resource limits, environment variables, and pilot-specific troubleshooting), see [Pilot Agent](/features/pilot-agent).
For a deep look at how the pilot tunnel works under the hood (credential lifecycle, security model, resource limits, environment variables, and pilot-specific troubleshooting), see [Pilot Agent](/features/pilot-agent). Cross-node container networking (reaching a meshed service on any node by hostname) rides the same authenticated channel and works in either mode; see [Sencho Mesh](/features/sencho-mesh).
## Add a remote node: Pilot Agent
@@ -63,17 +63,17 @@ On the control instance, click your avatar in the top-right and choose **Setting
Click **Add node**.
### Step 2. Run the enrollment command on the remote host
### Step 2. Deploy the agent on the remote host
A modal opens with a single `docker run` command. Copy it and run it on the remote host as the user that owns Docker.
A modal opens with a generated Compose file and a start command.
<Frame>
<img src="/images/multi-node/pilot-enroll.png" alt="Enroll the pilot agent modal showing a docker run command with SENCHO_MODE=pilot, SENCHO_PRIMARY_URL pointing to the control instance, and a SENCHO_ENROLL_TOKEN. The footer reads Expires 15m from now and offers a Copy command button." />
<img src="/images/multi-node/pilot-enroll.png" alt="Enroll the pilot agent modal with two steps. Step 1 shows a generated compose.yaml with SENCHO_MODE=pilot, SENCHO_PRIMARY_URL pointing to the control instance, and a SENCHO_ENROLL_TOKEN. Step 2 shows the docker compose up -d start command with a copy button. The footer reads Expires 14m from now and offers a Copy compose file button." />
</Frame>
The command is one line. It pulls the `saelix/sencho:latest` image, mounts the host Docker socket, and starts a container named `sencho-agent` that opens an outbound tunnel back to the control plane.
Save Step 1's contents as `compose.yaml` on the remote host, then run Step 2's command (`docker compose up -d`) in the same directory as the user that owns Docker. The generated file pulls the `saelix/sencho:latest` image, mounts the host Docker socket, attaches a named volume for the agent's own state so re-enrollment survives a container restart, and starts a container named `sencho-agent` that opens an outbound tunnel back to the control plane.
The enrollment token in the command is **valid for 15 minutes and can only be used once**. If you wait too long, follow the regeneration steps below.
The enrollment token embedded in the file is **valid for 15 minutes and can only be used once**. If you wait too long, follow the regeneration steps below.
### Step 3. Verify the tunnel
@@ -87,7 +87,7 @@ If the enrollment token expires, the container is removed, or the host is rebuil
<img src="/images/multi-node/edit-pilot-regenerate.png" alt="Edit node modal for a Pilot Agent node. Below the Compose Directory field a bordered card explains: 'Re-enroll the agent if the container was lost or the enrollment token expired. The previous tunnel is disconnected automatically.' A Regenerate enrollment token button sits below the explanation." />
</Frame>
Click the button to mint a fresh enrollment command. The previous tunnel is disconnected automatically; run the new command on the remote host to reconnect.
Click the button to mint a fresh enrollment Compose file. The previous tunnel is disconnected automatically; save and deploy the new file on the remote host the same way (`compose.yaml`, then `docker compose up -d`) to reconnect.
## Add a remote node: Distributed API Proxy
@@ -137,7 +137,7 @@ The **node switcher** sits at the top of the sidebar and acts as a persistent id
When two or more nodes are registered, clicking the switcher opens a popover listing every connected node. Each row shows a status dot, the name, and a metadata line that joins the type chip, the last-seen timestamp (pilot agents only), and the node version with middle-dot separators (for example, `AGENT · SEEN 13H AGO · v0.76.3`). The active node is marked with a brand-coloured rail, and a filled star calls out the default node.
<Frame>
<img src="/images/multi-node/node-switcher-dropdown.png" alt="Node switcher popover with a Connected header above an 8 NODES count. The Local row shows LOCAL · v0.76.3 with a star icon. Three Remote rows show the REMOTE chip. The sencho-pilot-test row shows AGENT · SEEN 13H AGO. A Manage nodes link sits at the bottom." />
<img src="/images/multi-node/node-switcher-dropdown.png" alt="Node switcher popover with a Connected header above a 4 NODES count. The Local row shows LOCAL · v0.95.0 with a star icon. Three Remote rows show the REMOTE chip. A Manage nodes link sits at the bottom." />
</Frame>
Click any row to switch. All views (dashboard, stack list, editor, resources, logs) immediately reflect the selected node. The **Manage nodes** link at the bottom of the popover opens the Settings → Nodes panel to add, edit, or remove nodes.
@@ -161,7 +161,7 @@ Node-level views (Home, Resources, Networking, App Store, Console, the editor) w
The Nodes table surfaces routing, status, and per-node automation at a glance for every node:
<Frame>
<img src="/images/multi-node/nodes-table-overview.png" alt="Nodes table close-up showing the ten columns: a default-marker column with a star on the Local row, Name, Type, Mode, Endpoint, Status, Labels, Schedules, Updates, and Actions. The pilot-test row has Mode 'Pilot Agent' and Endpoint 'tunnel (seen 13h ago)'. Other remotes show Mode 'Proxy' and full URLs in the Endpoint column." />
<img src="/images/multi-node/nodes-table-overview.png" alt="Nodes table close-up showing the ten columns: a default-marker column with a star on the Local row, Name, Type, Mode, Endpoint, Status, Labels, Schedules, Updates, and Actions. The Local row shows Endpoint 'docker.sock' and an Auto updates badge. Three Proxy-mode remote rows show full URLs in the Endpoint column." />
</Frame>
| Column | What it shows |
@@ -187,17 +187,22 @@ When you select a remote node in the switcher, the Settings hub filters to the p
| Panel | Scope | Notes |
|-------|:-----:|-------|
| Appearance | Per browser | Theme and density preferences are stored in your browser, not on the node. |
| Host Alerts | Per node | Host CPU, RAM, and disk thresholds for the selected node. |
| Channels | Per node | Discord, Slack, Webhook, and Apprise channels fire from the node that detects the event. |
| Labels | Per node | Stack and container label palettes. |
| Vulnerability Scanning | Per node | Trivy install state and scanner readiness for the selected node. |
| Developer | Per node | Retention windows for metrics and logs, plus Developer Mode. |
| Appearance | Per browser | Theme, density, and navigation preferences are stored in your browser, not on the node. |
| Stacks | Per node | Stack editor, lifecycle workflow preferences, and deploy guardrails for the selected node. |
| Fleet (Settings panel) | Per node | Fleet-snapshot documentation capture for Dossier notes on the selected node. Administrator only. Distinct from the hub-only **Fleet** nav view described below. |
| App Store | Per node | Template registry URL and featured-catalog source for the selected node's catalog. |
| Support | Per browser | Diagnostics bundle, docs links, contact channels. |
| About | Per browser | Build metadata for whichever instance the page is loaded from. |
| Host Alerts | Per node | Host CPU, RAM, and disk alert thresholds for the selected node. |
| Container Alerts | Per node | Crash, OOM, and healthcheck alert behavior for containers on the selected node. |
| Docker & Storage | Per node | Reclaimable-space alerts and image cleanup after updates on the selected node. |
| Channels | Per node | Discord, Slack, Webhook, and Apprise destinations that fire from whichever node detects the event. |
| Image update checks | Per node | How often the selected node polls registries for available image updates. |
| Labels | Per node | Stack and container label palettes for the selected node. |
| Data Retention | Per node | How long the selected node keeps container metrics, notification logs, scan history, and audit entries. |
| Developer Diagnostics | Per node | Developer mode for real-time metrics streams and verbose debug diagnostics on the selected node. |
| Support | Same everywhere | Diagnostics bundle, docs links, and contact channels; identical regardless of the active node. |
| About | Same everywhere | Build metadata, release notes, and licence attributions; identical regardless of the active node. |
Panels that manage control-plane concerns (Account, Admiral Account, Users, SSO, API Tokens, Registries, Recovery Vault, Nodes, Routing, Webhooks) are hidden when a remote node is active.
Panels that manage control-plane concerns (Account, Admiral Account, Users, SSO, API Tokens, Nodes, Registries, Recovery Vault, Routing, Mute Rules, Webhooks, Recovery) are hub-only and are hidden when a remote node is active.
## License enforcement across nodes
@@ -316,7 +321,7 @@ Sencho takes the opposite approach: infrastructure-level encryption (VPN, revers
Click **Test connection** on the row to re-probe and read the toast. The most common causes are: a wrong URL (missing scheme, missing port, trailing slash typo), a token that was rotated on the remote (any new **Generate Token** invalidates the previous one, so re-issue and update the saved row), and a firewall or reverse proxy that is not forwarding to port 1852 on the remote. Distributed API Proxy mode requires the control instance to reach the remote on the URL you saved; Pilot Agent mode requires the remote to reach the control instance on outbound HTTPS.
</Accordion>
<Accordion title="A pilot agent stays on `tunnel (waiting)`">
The agent has not dialled home yet. Three things can cause this. First, the enrollment token expired (15 minutes from generation): open **Edit node**, click **Regenerate enrollment token**, copy the new command, and run it on the remote host. Second, the `sencho-agent` container exited or never started; `docker ps -a` on the remote will show the exit code. Third, outbound HTTPS to the control instance is blocked, so check the remote's egress firewall or proxy.
The agent has not dialled home yet. Three things can cause this. First, the enrollment token expired (15 minutes from generation): open **Edit node**, click **Regenerate enrollment token**, save the new `compose.yaml`, and run `docker compose up -d` again on the remote host. Second, the `sencho-agent` container exited or never started; `docker ps -a` on the remote will show the exit code. Third, outbound HTTPS to the control instance is blocked, so check the remote's egress firewall or proxy.
</Accordion>
<Accordion title="Test Connection fails with 401 Unauthorized">
The bearer token saved for the row no longer matches what the remote will accept. This usually means somebody clicked **Generate Token** on the remote (which invalidates the previous token) or the row was saved with a typo. Generate a fresh token on the remote, click the pencil icon on the row in the control instance, paste the new token into the API Token field, and save.
@@ -325,6 +330,6 @@ Sencho takes the opposite approach: infrastructure-level encryption (VPN, revers
The control instance's license tier is authoritative for proxied requests, so a Community control plane gates Admiral features on every remote, even if the remote itself has its own Admiral license. Activate an Admiral license on the control instance to lift the gate fleet-wide. The reverse case (Admiral control plane, Community remote) works automatically because the control plane's tier is what the remote trusts.
</Accordion>
<Accordion title="Settings panels disappear when I switch to a remote">
That is intentional. Account, Admiral Account, Users, SSO, API Tokens, Registries, Recovery Vault, Nodes, Routing, and Webhooks are control-plane concerns and are hidden while a remote node is selected. Switch back to **Local** from the node switcher to manage them. The full list of which panels are per-node, per-browser, and control-plane-only lives in the [What Settings apply per node](#what-settings-apply-per-node) table above.
That is intentional. Account, Admiral Account, Users, SSO, API Tokens, Nodes, Registries, Recovery Vault, Routing, Mute Rules, Webhooks, and Recovery are control-plane concerns and are hidden while a remote node is selected. Switch back to **Local** from the node switcher to manage them. The full list of which panels are per-node, per-browser, and control-plane-only lives in the [What Settings apply per node](#what-settings-apply-per-node) table above.
</Accordion>
</AccordionGroup>
+25 -8
View File
@@ -7,7 +7,7 @@ When you manage [multiple nodes](/features/multi-node), each running its own Sen
## How it works
Every Sencho instance exposes a public `GET /api/meta` endpoint that advertises its version and a list of **capabilities**, one flag per feature it implements. When you switch to a node, your control instance fetches that node's metadata and caches it (briefly on the server, and for 5 minutes on success or 30 seconds on failure in the dashboard), so the version pill and the per-feature gates stay in sync without re-hitting the remote on every navigation. Testing a node from **Settings · Nodes**, or a node finishing an update, refreshes its metadata right away rather than waiting for the cache to expire.
Every Sencho instance exposes a public `GET /api/meta` endpoint that advertises its version and a list of **capabilities**, one flag per feature it implements. When you switch to a node, your control instance fetches that node's metadata and caches it (3 minutes on the server, and 5 minutes on success or 30 seconds on failure in the dashboard), so the version pill and the per-feature gates stay in sync without re-hitting the remote on every navigation. Testing a node from **Settings · Nodes**, or a node finishing an update, refreshes its metadata right away rather than waiting for the cache to expire.
Features whose capability is missing from the active node's list are replaced by a lock card naming the missing feature and prompting you to upgrade the node. Core features (stack management, containers, resources, logs) work on every Sencho version and are never gated this way.
@@ -22,17 +22,17 @@ Features whose capability is missing from the active node's list are replaced by
The node switcher popover shows each node's Sencho version next to its type chip, separated by a middle dot.
<Frame>
<img src="/images/node-compatibility/node-switcher-versions.png" alt="Node switcher popover listing seven nodes. The Local row reads LOCAL · v0.81.11 with a star icon for the default node. Three remote rows read REMOTE · v0.81.11. The sencho-pilot-test row reads AGENT · SEEN 20M AGO · v0.76.9, calling out that this node is on an older release than the rest of the fleet." />
<img src="/images/node-compatibility/node-switcher-versions.png" alt="Node switcher popover listing four connected nodes. The Local row reads LOCAL · v0.95.0 with a star icon for the default node. The Opsix row reads REMOTE · v0.95.0. The Pitt-Moba and SLX-Mars rows read REMOTE only, with no version pill, because this dashboard session has not switched to them yet." />
</Frame>
A node's version pill appears once Sencho has fetched its metadata. The metadata cache populates the first time you visit a node, so freshly-enrolled remotes may show their type chip alone until you switch to them once. A node that does not return metadata at all shows no version pill on either visit.
A node's version pill appears once Sencho has fetched its metadata. The metadata cache populates the first time you visit a node, so a node you have not switched to yet in the current session shows its type chip alone, with no version pill, until you do. A node that does not return metadata at all shows no version pill on either visit.
### Lock card on unsupported features
When the active node does not advertise a capability that a feature needs, the feature panel is replaced by a centered lock card.
<Frame>
<img src="/images/node-compatibility/lock-card.png" alt="Capability lock card centered in the editor area. A circular icon badge holds an Unplug glyph at the top. Below it, a title reads Host Console is not available on this node and a body line reads sencho-pilot-test is running v0.76.9. Upgrade the node to use this feature." />
<img src="/images/node-compatibility/lock-card.png" alt="Capability lock card centered in the editor area. A circular icon badge holds an Unplug glyph at the top. Below it, a title reads Audit Log is not available on this node and a body line reads Local is running v0.95.0. Upgrade the node to use this feature." />
</Frame>
The card carries the feature name, the node name, and (when known) the running version, so you can see at a glance what is missing and where. When the node has not reported a version, the body line drops the version and reads "*node-name* does not advertise this capability. Upgrade the node to use this feature." instead. The gated panel itself does not load; the lock card replaces it, which keeps slow capability-heavy views from briefly flashing into view before being hidden.
@@ -42,7 +42,7 @@ The card carries the feature name, the node name, and (when known) the running v
When you test a remote node from **Settings · Nodes**, the Connection Details panel that appears under the table lists the node's running Sencho version alongside its OS, architecture, container count, image count, and CPU count.
<Frame>
<img src="/images/node-compatibility/connection-test-version.png" alt="Connection Details panel for sencho-test-01 showing Instance: Remote Sencho, Sencho: v0.81.11, OS: Remote, Arch: Remote, Containers: 1, Images: 21, CPUs: 1, with a wifi-style icon in the heading." />
<img src="/images/node-compatibility/connection-test-version.png" alt="Connection Details panel for Opsix showing Instance: Remote Sencho, Sencho: v0.95.0, OS: Remote, Arch: Remote, Containers: 4, Images: 4, CPUs: 2, with a wifi-style icon in the heading." />
</Frame>
This is the most direct way to confirm what version a remote node is on without switching to it first.
@@ -66,6 +66,7 @@ Every Sencho release ships with a static list of capabilities. The current list
| `network-topology` | Network topology view |
| `notifications` | Alert notifications |
| `notification-routing` | Notification routing rules |
| `notification-suppression` | Mute rules |
| `host-console` | Host Console |
| `container-exec` | Container exec terminal |
| `audit-log` | Audit log |
@@ -77,15 +78,31 @@ Every Sencho release ships with a static list of capabilities. The current list
| `self-update` | Self-update from the dashboard |
| `vulnerability-scanning` | Image vulnerability scanning |
| `compose-doctor` | Compose Doctor preflight |
| `compose-networking` | Stack Networking tab and node Networking overview |
| `update-guard` | Pre-update rollback-readiness dialog |
| `compose-networking` | Stack Networking tab |
| `env-inventory` | Stack Environment tab (env var inventory) |
| `container-label-inventory` | Stack Compose Labels tab and Fleet label filters |
| `project-env-files` | Project-level env file selection in the Environment tab |
| `compose-storage` | Stack Storage tab (volume explorer) |
| `cross-node-rbac` | Role enforcement on requests forwarded to a remote node |
| `stack-down-remove-volumes` | The "also remove volumes" option on Take Down |
| `guided-external-network-preflight` | Guided missing-external-network check before deploy |
| `service-scoped-update` | Per-service update, rebuild, and restore on multi-service stacks |
<Note>
`vulnerability-scanning` is advertised only when the Trivy binary is present on the node. If a node does not have Trivy installed, it omits this capability and the scanning UI is replaced by a lock card.
`vulnerability-scanning` is advertised only when the Trivy binary is present on the node. If a node has no scanner installed, it omits this capability and the scanning UI is replaced by a lock card whose body reads "Install a scanner from the Scanner setup tab." instead of the generic upgrade message.
</Note>
Nodes that do not advertise `guided-external-network-preflight` keep legacy deploy behavior (no guided missing-external dialog). When the capability is present but the check cannot run, deploy fails closed instead of guessing.
<Note>
A node connected in [Pilot Agent](/features/multi-node#add-a-remote-node-pilot-agent) mode never advertises `host-console`, even after upgrading, because that feature's control-to-agent path is not yet wired through the enrollment tunnel. This is the one capability gap that upgrading the node cannot close; it only closes on a Distributed API Proxy connection.
</Note>
A handful of capabilities gate a smaller piece of behavior rather than a whole panel, so a missing one falls back to an older behavior instead of a lock card:
- Nodes that do not advertise `guided-external-network-preflight` keep legacy deploy behavior (no guided missing-external dialog). When the capability is present but the check cannot run, deploy fails closed instead of guessing.
- Nodes that do not advertise `service-scoped-update` fall back to the legacy per-container layout on multi-service stacks: no declared-service headers, and no per-service update, rebuild, or restore.
- Nodes that do not advertise `update-guard` run manual updates directly, with no pre-update rollback-readiness dialog.
- `cross-node-rbac` is a security boundary, not a convenience: the control instance refuses to forward a non-admin request, or a confirmed stop-by-label, to a remote node that does not advertise it. This stops a mixed-version fleet from letting a lower-privileged action escalate on an un-upgraded node.
## Handling nodes that do not advertise metadata
+1
View File
@@ -1,5 +1,6 @@
---
title: Product Guide
sidebarTitle: Overview
description: A high-level tour of Sencho, organized by the same areas as the product guide sidebar.
---
+7 -7
View File
@@ -105,7 +105,7 @@ When you submit the Add Node form with Mode set to Pilot Agent, the control inst
3. Shows the compose file in an enrollment dialog with a 15-minute countdown.
<Frame>
<img src="/images/pilot-agent/02-enrollment-dialog.png" alt="Enroll the pilot agent modal with kicker 'NODES · PILOT ENROLLMENT'. Step 1 saves a Docker Compose file as 'compose.yaml' with the env block showing SENCHO_MODE: pilot, SENCHO_PRIMARY_URL: https://sencho.example.com, and SENCHO_ENROLL_TOKEN: <short-lived token>. Step 2 shows the 'docker compose -f compose.yaml up -d' command. Footer reads 'Expires 14m from now' alongside a 'Copy compose file' button." />
<img src="/images/pilot-agent/02-enrollment-dialog.png" alt="Enroll the pilot agent modal with kicker 'NODES · PILOT ENROLLMENT'. Step 1 saves a Docker Compose file as 'compose.yaml' with the env block showing SENCHO_MODE: pilot, SENCHO_PRIMARY_URL: https://sencho.example.com, and SENCHO_ENROLL_TOKEN: <short-lived token>. Step 2 shows the 'docker compose up -d' command. Footer reads 'Expires 14m from now' alongside a 'Copy compose file' button." />
</Frame>
The enrollment token is one-time and short-lived by design. If a token is intercepted or leaked, the window of risk is small and the slot is consumed on first use.
@@ -138,7 +138,7 @@ volumes:
Save it as `compose.yaml` on the remote host and bring the agent up from the same directory:
```bash
docker compose -f compose.yaml up -d
docker compose up -d
```
Deploying through Compose also lets the control instance push over-the-air updates to the agent later from the Fleet view ([Remote Updates](/features/remote-updates)), without manual intervention on the remote host. Compose-managed containers carry the labels Sencho needs to recreate them in place.
@@ -163,7 +163,7 @@ The control instance pings every connected agent every 30 seconds. Any stream wi
- **Control instance restarts**: every agent's tunnel drops cleanly. Each agent reconnects within seconds. In-flight HTTP requests fail and the browser retries; long-lived WebSockets (logs, console) reconnect.
- **Agent restarts**: the persisted token re-authenticates without operator intervention. Same reconnect-and-recover behavior.
- **Operator deletes the node on the control instance**: the agent's existing tunnel is closed. The agent will keep trying to reconnect with its persisted token, which the control instance no longer accepts; the agent stays in a reconnect-backoff loop. To stop it, tear the agent down on the remote with `docker compose -f compose.yaml down -v`.
- **Operator deletes the node on the control instance**: the agent's existing tunnel is closed. The agent will keep trying to reconnect with its persisted token, which the control instance no longer accepts; the agent stays in a reconnect-backoff loop. To stop it, tear the agent down on the remote with `docker compose down -v`.
### Re-enrollment
@@ -247,7 +247,7 @@ Each pilot tunnel has fixed protocol-level ceilings so one misbehaving agent can
| **Tunnel write buffer pause** | 4 MB | High-water mark for the agent-bound write queue | When the control instance's queue is over the mark, inbound HTTP request bodies are paused and resumed on drain. Backpressure rather than memory growth. |
| **Enrollment rate** | 10 per minute | Operator-issued enrollment tokens | Protects the database against a script accidentally minting tokens in a loop. |
An admin-only diagnostic endpoint, `GET /api/system/pilot-tunnels`, returns counters and a per-node breakdown of active tunnels (open stream count and `bufferedAmount` for each). Useful for triaging a single sticky tunnel inside an otherwise healthy fleet.
An admin-only diagnostic endpoint, `GET /api/system/pilot-tunnels`, returns counters and a per-node breakdown of active tunnels (connect time and `bufferedAmount` for each). Useful for triaging a single sticky tunnel inside an otherwise healthy fleet.
## Environment variables
@@ -276,7 +276,7 @@ These are the boundaries operators should know about before designing a fleet ar
- **No mode conversion.** The Edit dialog shows a Mode field for an enrolled node, but switching a node between Pilot Agent and Distributed API Proxy after enrollment leaves the credentials and connection state inconsistent. To change modes, delete the node and re-create it in the desired mode.
- **No audit log entries for enrollment lifecycle.** Node creation, enrollment regeneration, and node deletion do not write to the audit log today. This is on the roadmap.
- **`pilot.jwt` is not cleaned up on node deletion.** When you delete a node from the control instance, the agent's persisted token stays on the remote's data volume. The agent will fail to reconnect on next restart, but the file persists. If you are repurposing the host, tear the agent down with `docker compose -f compose.yaml down -v` to remove the `sencho-agent-data` volume.
- **`pilot.jwt` is not cleaned up on node deletion.** When you delete a node from the control instance, the agent's persisted token stays on the remote's data volume. The agent will fail to reconnect on next restart, but the file persists. If you are repurposing the host, tear the agent down with `docker compose down -v` to remove the `sencho-agent-data` volume.
- **JWT-secret rotation invalidates existing tunnel JWTs.** Rebuilding the control instance from scratch or rotating `auth_jwt_secret` requires a fresh enrollment token on each agent. When that token is still in the container environment, the agent re-enrolls automatically without manual deletion of `pilot.jwt`.
- **One tunnel per node.** Splitting a node's load across multiple control instances or running multiple agent containers against the same control instance for the same node is not supported.
- **Mesh and pilot share the per-tunnel stream pool.** A node that runs heavy Sencho Mesh traffic counts those streams against the same 1024-stream cap as HTTP and WebSocket traffic.
@@ -308,7 +308,7 @@ The generic node-connectivity issues (a node showing Offline, a pilot agent stuc
</Accordion>
<Accordion title="I ran the enrollment command on the wrong host">
Tear the agent down on the wrong host (`docker compose -f compose.yaml down -v` from the directory you saved the compose file in), then regenerate the enrollment token on the control instance and deploy the new compose file on the correct host.
Tear the agent down on the wrong host (`docker compose down -v` from the directory you saved the compose file in), then regenerate the enrollment token on the control instance and deploy the new compose file on the correct host.
</Accordion>
<Accordion title="HTTP 429 'Too many enrollment requests'">
@@ -320,7 +320,7 @@ The generic node-connectivity issues (a node showing Offline, a pilot agent stuc
</Accordion>
<Accordion title="I deleted the node on the control instance but the agent keeps trying to reconnect">
Deleting the node removes the routing entry on the control instance but does not touch the remote. The agent still holds `/app/data/pilot.jwt` and will keep dialing. Tear the agent down on the remote (`docker compose -f compose.yaml down -v` from the directory holding the compose file) so the `sencho-agent-data` named volume that stores the persisted token is removed.
Deleting the node removes the routing entry on the control instance but does not touch the remote. The agent still holds `/app/data/pilot.jwt` and will keep dialing. Tear the agent down on the remote (`docker compose down -v` from the directory holding the compose file) so the `sencho-agent-data` named volume that stores the persisted token is removed.
</Accordion>
<Accordion title="Stacks tab on a pilot node shows nothing or fails to list">
+20 -13
View File
@@ -11,6 +11,10 @@ This page covers the mechanism: prerequisites, what happens on a node during an
Triggering an update (per-row or **Update all**) requires the **admin** role. Viewer and operator roles can read update status but cannot dispatch updates.
</Note>
<Note>
This is the Sencho self-update feature, reached from the Fleet tab. The top-level **Update** nav item is a different feature: it tracks pending Docker image updates for your own stacks (Plex, Radarr, and the rest), not the Sencho binary itself. See [Health-Gated Updates](/features/health-gated-updates) for that page.
</Note>
## Prerequisites
A node can self-update when all of the following are true:
@@ -23,13 +27,13 @@ Nodes deployed with `docker run`, with hand-rolled systemd units, or with orches
## Anatomy at a glance
Open the **Fleet** tab and click **Check Updates** in the page header to open the **Node updates** sheet. The sheet lists every registered node with its current and latest Sencho version, the cluster-wide summary, and the action buttons.
Open the **Fleet** tab and click **Node Update** in the Overview toolbar to open the **Node updates** sheet. The sheet lists every registered node with its current and latest Sencho version, the cluster-wide summary, and the action buttons, split across two tabs: **Nodes** (the table and summary) and **Changelog** (release notes for the latest published version, pulled from GitHub Releases).
<Frame>
<img src="/images/fleet-view/fleet-node-updates.png" alt="Node updates sheet titled 'Node updates · 8 nodes · 8 updates available'. Recheck and 'Update all (7)' buttons sit under the header. Four summary cards read '0 Up to date', '8 Available', '0 Updating', '0 Failed'. A 'Filter nodes…' search box sits above a table with columns Node / Type / Current / Latest / Status. Eight rows show every node at v0.76.6 with an Update button on the right. The sencho-pilot-test row reports Current as 'unknown'. The footer reads 'LATEST VERSION v0.76.7'." />
<img src="/images/fleet-view/fleet-node-updates.png" alt="Node updates sheet titled 'Node updates · 4 nodes · 0 updates available'. A Recheck button sits under the header, above Nodes and Changelog tabs. Four summary cards read '4 Up to date', '0 Available', '0 Updating', '0 Failed'. A 'Filter nodes…' search box sits above a table with columns Node / Type / Current / Latest / Status; all four rows (Local, Opsix, Pitt-Moba, SLX-Mars) show a green 'Up to date' badge at v0.95.0. The footer reads 'LATEST VERSION v0.95.0'." />
</Frame>
For the per-control breakdown of the sheet (header actions, summary cards, table columns, footer), see [Fleet View · Node Updates](/features/fleet-view#node-updates).
For the per-control breakdown of the sheet (header actions, summary cards, table columns, skip/unskip, changelog tab), see [Fleet View · Node Updates](/features/fleet-view#node-updates).
## Triggering a remote update
@@ -38,25 +42,25 @@ There are two surfaces that initiate an update on a remote node:
- The **Update** button on a row inside the Node updates sheet.
- The **Update to vX.Y.Z** outline button along the bottom of any online node card on the Fleet grid.
The **Skip** button next to Update hides the update prompt for that node and excludes it from **Update all** until a newer version is released. The skip is per-node and per-version: when the next Sencho release comes out, the skip clears automatically and the update surfaces again. Skipping requires the admin role.
The **Skip** button next to Update hides the update prompt for that node and excludes it from **Update all** until a newer version is released; the row shows a **Skipped vX.Y.Z** badge with an **Unskip** button to bring the prompt back early. The skip is per-node and per-version: when the next Sencho release comes out, the skip clears automatically and the update surfaces again. Skipping and unskipping both require the admin role.
<Frame>
<img src="/images/fleet-view/node-card-update-available.png" alt="Node card for the Opsix remote showing an Online badge, the version chip 'v0.76.6', a warning 'Update available' pill, the Running / Stopped / Stacks counters (4 / 0 / 3), CPU / RAM / Disk usage bars, and an outline 'Update to v0.76.7' button across the bottom of the card." />
<img src="/images/fleet-view/node-card-opsix-online.png" alt="Node card for the Opsix remote showing an Online badge, the version chip 'v0.95.0', the Running / Stopped / Stacks counters (4 / 0 / 3), and CPU / RAM / Disk usage bars." />
</Frame>
Either surface dispatches the same backend call. While the call is in flight, the button reads **Triggering...** with a spinning icon, then settles into the **Updating** badge once the gateway has accepted the request.
When an update is available, this same card gains a warning **Update available** pill next to the version chip and an outline **Update to vX.Y.Z** button across the bottom (not pictured above, since every node in this fleet is currently up to date). Either surface dispatches the same backend call. While the call is in flight, the button reads **Triggering...** with a spinning icon, then settles into the **Updating** badge once the gateway has accepted the request.
The gateway switches to a fast 5-second polling loop while any node is in the `Updating` state, so the badge advances in near real time without waiting for the next 30-second fleet refresh.
## Updating the local (gateway) node
Updating the gateway is special because the dashboard is hosted by the very container that is about to restart. Clicking **Update** on the local row, or **Update to vX.Y.Z** on the Local card, opens a confirmation step before anything happens on disk.
Updating the gateway is special because the dashboard is hosted by the very container that is about to restart. Clicking **Update** on the local row, or **Update to vX.Y.Z** on the Local card, opens a confirmation dialog (kicker **LOCAL · UPDATE**, title **Update local node**, with **Cancel** and **Update & restart** buttons) before anything happens on disk. The body text depends on how the compose file pins the image: for a semver pin it names the exact rewrite (for example, "This install pins `saelix/sencho:0.94.1`. Updating rewrites it to `saelix/sencho:0.95.0`..."); for a floating tag it reads more generally ("Pulls Sencho v0.95.0 and restarts the server..."). Both variants end with the same note that the dashboard briefly disconnects and reconnects automatically.
<Frame>
<img src="/images/fleet-view/local-update-confirm.png" alt="Alert dialog with kicker 'LOCAL · UPDATE', heading 'Update local node', body 'Pulls the latest Sencho image and restarts the server. The dashboard briefly disconnects and reconnects automatically when the update completes.', and two buttons: Cancel and 'Update & restart'." />
</Frame>
<Note>
If the local node is running the Admiral **Hardened Build** image instead of a Community image, clicking **Update** here runs a different, entitlement-gated switch flow instead of the compose-repin steps below: it requires an authenticated browser session (not an API token) and can fail with its own codes (`entitlement_denied`, `preflight_mismatch`, `compose_unavailable`, `registry_access_unavailable`) surfaced on the same **Failed** badge. See [Licensing · Feature breakdown](/features/licensing#feature-breakdown) for what Hardened Build is.
</Note>
After **Update & restart** is confirmed:
After **Update & restart** is confirmed on a Community-channel install:
1. The browser captures the gateway's current boot timestamp from `/api/health`.
2. The server classifies the compose image pin. For semver pins it pulls the target release, stages a rewritten compose file, and spawns a short-lived helper container that copies the file onto the host and runs `docker compose up -d --force-recreate`.
@@ -65,7 +69,7 @@ After **Update & restart** is confirmed:
If the pull or compose rewrite fails before the container restarts, the overlay dismisses within a few seconds and surfaces the error in a toast instead of waiting for the full reconnect timeout.
If the new container does not come up within 5 minutes, the overlay surfaces an **Update timed out** message with a *Try Reloading* button so the page is never left waiting indefinitely. If the gateway can detect that the update did not even start (for example, the image pull failed before the helper container could spawn) it surfaces a **Failed** badge with the underlying error on the Local card. The badge appears as soon as the helper writes its error file, or by the 3-minute mark at the latest, instead of waiting for the full 5-minute timeout.
If the new container does not come up within 5 minutes, the overlay surfaces a **Taking longer than expected** message with a **Reload to check** button so the page is never left waiting indefinitely; this is not necessarily a failure, since a large image pull can legitimately run past that window. If the gateway can detect that the update did not even start (for example, the image pull failed before the helper container could spawn) it surfaces a **Failed** badge with the underlying error on the Local card. The badge appears as soon as the helper writes its error file, or by the 3-minute mark at the latest, instead of waiting for the full 5-minute timeout.
<Note>
The self-update helper container inherits all bind mounts from the main Sencho container 1:1. If your `docker-compose.yml` references `env_file`, `configs`, or `secrets` outside the compose working directory, those host paths must be mounted into the Sencho container at the *same container path* as on the host. See [Troubleshooting](/operations/troubleshooting#local-self-update-fails-with-env-file-not-found) if you encounter `env file not found` errors during a local update.
@@ -77,6 +81,9 @@ If the new container does not come up within 5 minutes, the overlay surfaces an
Fleet self-update respects how each node compose file declares the Sencho image. Semver pins are rewritten to the target release before recreate. Floating tags such as latest are pulled without changing the compose file. Digest pins and unresolved interpolated values block automatic updates; those rows show a **Pinned** badge instead of an **Update** button.
The image pull always runs before any compose rewrite, so a failed pull never leaves the compose file half-updated.
A node running the Admiral **Hardened Build** image is a deliberate exception: it is digest-pinned like any other locked reference, but it does not show the **Pinned** badge and its **Update** button stays available, because that path routes through the Hardened Build's own entitlement-gated switch instead of a compose repin. See the note in [Updating the local (gateway) node](#updating-the-local-gateway-node) above.
## What happens during an update
For both local and remote nodes, an update goes through the same three steps:
@@ -123,7 +130,7 @@ The header of the sheet exposes a **Recheck** button that does three things in o
The self-update helper container is launched with the same bind-mount set as the main Sencho container, but only paths that are explicitly mounted are visible inside the helper. If your `docker-compose.yml` references an `env_file` (or `configs`, or `secrets`) at a host path that is *not* mounted into the Sencho container, the helper cannot read it and the recreate step fails. Mount the referenced host directory into the Sencho container at the same container path (the 1:1 rule), redeploy Sencho once, and retry the update. The full walk-through with example mounts is in [Troubleshooting](/operations/troubleshooting#local-self-update-fails-with-env-file-not-found).
</Accordion>
<Accordion title="The reconnecting overlay never clears">
The overlay polls `/api/health` every 3 seconds for up to 5 minutes. If it has not cleared by then, the new container is either still pulling a very large image, refusing to start, or crashing on boot. Open a host shell on the gateway and run `docker logs <sencho-container>` to inspect the boot output, then `docker ps` to confirm a new container ID is running. The overlay's *Try Reloading* button bypasses the timer once the container is healthy again.
The overlay polls `/api/health` every 3 seconds for up to 5 minutes. If it has not cleared by then, the new container is either still pulling a very large image, refusing to start, or crashing on boot. Open a host shell on the gateway and run `docker logs <sencho-container>` to inspect the boot output, then `docker ps` to confirm a new container ID is running. The overlay's **Reload to check** button bypasses the timer once the container is healthy again.
</Accordion>
<Accordion title="A remote shows 'Failed' after about 3 minutes">
The remote answered the dispatch but did not restart in time. The two most common causes are a registry pull error (rate limit, private-registry auth missing, network egress blocked) or a Compose file that fails validation under the new version. Open a shell on the remote and run `docker compose pull` followed by `docker compose up -d` against the Sencho working directory; the error printed there is the same one Sencho captured. Click **Retry** on the row once the cause is fixed.
+25 -22
View File
@@ -6,12 +6,12 @@ description: Browse, filter, scan, and clean up Docker images, volumes, and unma
The **Resources** tab shows everything Docker is storing on your host, broken down by ownership, with one-click cleanup and on-demand vulnerability scanning.
<Frame>
<img src="/images/resources/resources-reclaim.png" alt="Resources Hub with the You can reclaim banner, Docker Disk Footprint treemap, Quick Clean panel, and the Images tab populated below" />
<img src="/images/resources/resources-reclaim.png" alt="Resources Hub with the You can reclaim banner, Docker Disk Footprint tiles, Quick Clean panel, and the Images tab populated below" />
</Frame>
## Reclaim hero
When there is reclaimable disk space (unused images, stopped containers, or dangling volumes), an amber banner leads the view with the total amount you can free and a `·`-separated breakdown of what contributes to it (for example, `16 unused images · 10 dangling volumes`). Click **Review & prune** to open a confirmation dialog that lists the exact items that will be removed (capped in the preview with an "and N more" note when the list is long). Confirm only after the plan is ready; Sencho rechecks the list at execute time and skips anything that is no longer eligible.
When there is reclaimable disk space (unused images, stopped containers, or dangling volumes), an amber banner leads the view with the total amount you can free and a `·`-separated breakdown of what contributes to it (for example, `2 unused images · 10 dangling volumes`). Click **Review & prune** to open a confirmation dialog that lists the exact items that will be removed (capped in the preview with an "and N more" note when the list is long). Confirm only after the plan is ready; Sencho rechecks the list at execute time and skips anything that is no longer eligible.
The hero stays hidden when there is nothing to reclaim, keeping the view focused on the rest of your inventory.
@@ -25,15 +25,15 @@ To keep the banner off for a node entirely, open **Settings → Monitoring → D
## Docker Disk Footprint
The left card under the hero shows how your Docker disk usage is distributed:
The left card under the hero breaks down your Docker disk usage into three tiles:
| Segment | Meaning |
|---------|---------|
| Tile | Meaning |
|------|---------|
| **Sencho managed** (green) | Images and volumes used by stacks in your `COMPOSE_DIR` |
| **External** (amber) | Images and volumes used by Docker projects outside Sencho |
| **Reclaimable** (neutral) | Unused images, stopped containers, and dangling volumes safe to delete |
Each segment shows its size and its share of the total footprint. Tile area is proportional to bytes. Click any segment to filter the resource tabs below to that category.
Each tile shows its size and its share of the total footprint as a percentage. The layout itself is fixed (Sencho managed on the left, External and Reclaimable stacked on the right); tile size does not scale with the percentage, so read the numbers rather than the proportions. Click any tile to filter the resource tabs below to that category.
## Quick Clean
@@ -56,11 +56,13 @@ A confirmation dialog appears before any destructive prune. Sencho first builds
## Resource tabs
Below the hero, three tabs partition your inventory: **images**, **volumes**, and **Unmanaged**. The Unmanaged tab shows a count badge whenever orphan containers are detected.
Below the hero, three tabs partition your inventory: **Images**, **Volumes**, and **Unmanaged**. The Unmanaged tab shows a count badge whenever orphan containers are detected.
Network inventory, topology, inspect, and findings live on the dedicated [Networking](/features/networking) page. **Quick Clean** still includes **Prune Dead Networks**.
A **Scan history** button sits on the right of the tab strip when image vulnerability scanning is configured for the node. It takes you to the [Security page](/features/security) History tab so you can review past results without launching a new scan. See [Vulnerability scanning](/features/vulnerability-scanning) for the full workflow.
Each of the first three tabs shares the same toolbar pattern: a collapsible search (click the magnifying glass to open a text filter, matching by name or repository tag) and an **All / Managed / External** filter with a live count on each chip. Sortable columns toggle ascending/descending order when clicked.
A **Scan history** button sits on the right of the Images tab's toolbar when image vulnerability scanning is configured for the node. It takes you to the [Security page](/features/security) History tab so you can review past results without launching a new scan. See [Vulnerability scanning](/features/vulnerability-scanning) for the full workflow.
<Note>
Sencho protects its own image, network, and named volumes from accidental deletion. The matching rows carry a **Sencho** pill alongside the managed status and the delete control is disabled.
@@ -68,14 +70,14 @@ A **Scan history** button sits on the right of the tab strip when image vulnerab
### Images
Lists all Docker images on the host with their ID, repository tag, size, and status.
Lists all Docker images on the host with their ID, repository tag, size, and status. Click **Repository:Tag**, **Size**, or **Status** to sort.
**Filter chips:** `All`, `Managed`, `External`. Each chip shows the matching count, for example `All 31 · Managed 14 · External 17`.
**Filter chips:** `All`, `Managed`, `External`. Each chip shows the matching count, for example `All 18 · Managed 15 · External 3`.
**Status column** combines two pieces of information per row:
- A usage badge: `In Use` plus one chip per Sencho stack that uses the image (click a chip to open that stack), `In Use` plus `External` when it runs only in another Docker project, or `Unused` when no container references it.
- A severity badge from any image scan: `Clean`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`, or `UNKNOWN`. Click the badge to open the scan results.
- A severity badge from any image scan: `Clean` (no findings at all), a CVE severity (`LOW`, `MEDIUM`, `HIGH`, `CRITICAL`, or `UNKNOWN`), or `Findings` (no CVEs, but a secret or misconfiguration finding). Click the badge to open the scan results.
**Per-row actions** sit on the right:
@@ -87,36 +89,37 @@ Lists all Docker images on the host with their ID, repository tag, size, and sta
Click the eye icon on any image row to open a detail sheet with these sections:
<Frame>
<img src="/images/resources/resources-image-inspect.png" alt="Image inspect sheet showing the Used by, Overview, Config, and Layers sections for a running image" />
</Frame>
- **Used by** lists every Sencho stack that references the image (chips open that stack). Sourced from the Resources inventory, so it stays visible while inspect loads or if inspect fails.
- **Overview** lists the ID, size, creation date, architecture and OS, author, and all repository tags.
- **Overview** lists the ID (with a copy button), size, creation date, architecture and OS, author, and all repository tags.
- **Config** shows the default `Cmd`, `Entrypoint`, `WorkingDir`, `User`, exposed ports, environment variables, and labels. Env vars and labels are collapsible.
- **Layers** lists the layer history in build order. Each row shows the layer index, size, age, and the build command (`CreatedBy`). Empty layers (metadata-only, zero bytes) are dimmed.
- **Registry tags** (admins) lists remote tag names from a matching configured registry. See [Private registries](/features/private-registries#browse-tags-from-resources) for credential and failure behavior. Vulnerability history and caching use the image digest when available; see [Tags vs digests](/features/vulnerability-scanning#tags-vs-digests).
- **Registry tags** (admins) lists remote tag names from a matching configured registry, with a **Load more** button when the registry has additional pages. See [Private registries](/features/private-registries#browse-tags-from-resources) for credential and failure behavior. Vulnerability history and caching use the image digest when available; see [Tags vs digests](/features/vulnerability-scanning#tags-vs-digests).
The inspect sheet is available to all roles. Registry tag browsing is admin-only.
### Volumes
<Frame>
<img src="/images/resources/resources-volumes-tab.png" alt="Volumes tab with Largest 5 and Recently changed landing cards above the volume list" />
<img src="/images/resources/resources-volumes-tab.png" alt="Volumes tab with the search and filter toolbar above the volume list" />
</Frame>
Lists all Docker volumes with name, driver, mount point, and managed status.
A two-card landing strip sits above the table:
- **Largest 5** ranks volumes by on-disk size, with the total volume count alongside.
- **Recently changed** highlights volumes created or touched in the last 24 hours, with relative timestamps such as `24m ago`.
Lists all Docker volumes with name, driver, mount point, and managed status. Click **Name** or **Driver** to sort.
**Filter chips:** `All`, `Managed`, `External`, each with its count.
Anonymous volumes (Docker-generated 64-character hash names, typical of a container that declares a volume without naming it) show a shortened label with an **anonymous** chip so the table stays readable; hover the name for the full hash.
<Warning>
Deleting a volume is permanent. Any data stored in it will be lost. Always back up important volume data before pruning.
</Warning>
#### Browse volume contents
Click the folder icon on any volume row (admin-only) to open a read-only browser for that volume. The panel shows a directory tree on the left and a file viewer on the right.
Click the folder icon on any volume row (admin-only) to open a read-only browser for that volume. The panel shows a directory tree on the left and a file viewer on the right. For an anonymous volume, a band above the tree shows the full 64-character name with a copy button, since the sheet's title and breadcrumb only show the shortened form.
- **Read only.** No edits, no writes, no deletions. The browser launches a short-lived helper container with the volume mounted read-only.
- **Size cap.** Files are read up to 5 MB. Larger files show a truncation banner.
@@ -131,7 +134,7 @@ Click the folder icon on any volume row (admin-only) to open a read-only browser
### Unmanaged
<Frame>
<img src="/images/resources/resources-unmanaged-tab.png" alt="Unmanaged tab with Select all checkbox, Purge Selected action, and a container row grouped under External Project: sencho" />
<img src="/images/resources/resources-unmanaged-tab.png" alt="Unmanaged tab showing the No unmanaged containers success state" />
</Frame>
Lists containers running on the host that are not part of any Sencho-managed stack: containers started with `docker run`, or Compose projects outside your `COMPOSE_DIR`. The tab shows a count badge when any are detected. The running Sencho container itself is excluded from this list so it cannot be purged by accident.
+19 -13
View File
@@ -18,7 +18,7 @@ Schedules is a unified surface for every recurring maintenance operation Sencho
Open the **Schedules** tab from the top navigation bar. The page opens on the Timeline view with the masthead, the five lane track, and a bottom time axis.
<Frame>
<img src="/images/scheduled-operations/timeline.png" alt="Schedules page in Timeline view. Header reads 'Scheduled Operations' on the left with a clock icon; on the right are a Timeline / All tasks toggle, a Refresh button, and a 'New Schedule' primary button. Below, a kicker 'NEXT 24 HOURS' sits above an italic display heading 'Next 24 hours' and a monospace date range 'Thu, May 14 11:21 → Fri, May 15 11:21'. A right-aligned 'Next' pill shows '03:00' with the subtitle 'Vul Scan · in 15h 38m'. Five lanes (Stack lifecycle, Updates, Security, Maintenance, Backups) run horizontally; a purple '03:00 hub' pill sits on the Security lane and a cyan '04:00 Entire fleet' pill sits on the Backups lane. A cyan vertical 'now' rail glows at the left edge. Six time ticks (11:21, 16:09, 20:57, 01:45, 06:33, 11:21) run along the bottom axis." />
<img src="/images/scheduled-operations/timeline.png" alt="Schedules page in Timeline view. Header reads 'Scheduled Operations' with a clock icon; on the right are a Timeline / All tasks toggle, a Refresh button, and a 'New Schedule' primary button. Below, a kicker 'NEXT 24 HOURS' sits above an italic display heading 'Next 24 hours' and a monospace date range. A right-aligned 'Next' pill shows the next firing's time, task name, and a relative countdown. Five lanes (Lifecycle, Updates, Security, Upkeep, Backups) run horizontally with color-coded dot legends; a blue pill sits on the Lifecycle lane, a green pill on the Updates lane, a purple pill on the Security lane, and a cyan pill on the Backups lane, each labelled with its firing time and target. The Upkeep lane has no pill because its only task is scheduled for a day outside the 24-hour window. A cyan vertical 'now' rail glows at the left edge. Six monospace time ticks run along the bottom axis." />
</Frame>
## Timeline view
@@ -40,7 +40,7 @@ Toggle to **All tasks** from the masthead to see every schedule in a table regar
The All tasks toggle swaps the lane track for a sortable table.
<Frame>
<img src="/images/scheduled-operations/all-tasks.png" alt="All tasks table view of Scheduled Operations. Columns are Name, Action, Target, Schedule, Status, Next Run, Enabled, Actions. Two rows are visible: 'Vul Scan' with a Scan Node Images badge, target 'system', schedule 'At 03:00 AM / 0 3 * * *', Success status, next run '5/15/2026, 3:00:00 AM', ON toggle, and an action group of Run now / Execution history / Edit / Delete (Delete in destructive red). 'Nightly Snapshot' with a Create Fleet Snapshot badge, target 'fleet', schedule 'At 04:00 AM / 0 4 * * *', Success, next run '5/15/2026, 4:00:00 AM', ON, same action group." />
<img src="/images/scheduled-operations/all-tasks.png" alt="All tasks table view of Scheduled Operations. Columns are Name, Action, Target, Schedule, Status, Next Run, Enabled, Actions. Five rows are visible, with Action badges Create Fleet Snapshot, Prune Node Resources, Scan Node Images, Restart Stack, and Auto-update All Stacks on Node, targeting fleet, system, system, a stack name, and all eligible stacks respectively. Each Schedule cell shows a human-readable description above its raw cron expression. Status shows Never run for four rows and a green Success badge for one. Every row has an ON toggle and an action group of Run now / Execution history / Edit / Delete (Delete in destructive red)." />
</Frame>
| Column | What it shows |
@@ -77,7 +77,7 @@ The All tasks toggle swaps the lane track for a sortable table.
Click **New Schedule** in the header. The form opens in a centered modal. The Action picker lists every supported operation, grouped by category: Lifecycle, Updates, Security, Upkeep, and Backups.
<Frame>
<img src="/images/scheduled-operations/action-picker.png" alt="The New scheduled task modal with the Action combobox expanded. The dropdown groups actions under category headers: Stack lifecycle (Backup Stack Compose Files, Start / Bring Up Stack, Restart Stack, Stop Stack, Take Stack Down), Updates (Auto-update Stack, Auto-update All Stacks on Node), Security (Scan Node Images), Maintenance (Prune Node Resources), and Backups (Create Fleet Snapshot). Below the picker, partly visible, sit a Services row with 'echo' and 'prober' checkboxes, the Cron Expression input, the Enabled toggle, and the Delete after successful run checkbox." />
<img src="/images/scheduled-operations/action-picker.png" alt="The New scheduled task modal with the Action combobox expanded. A floating list shows the 'Lifecycle' category header above Backup Stack Compose Files, Start / Bring Up Stack, a checked Restart Stack, Stop Stack, and Take Stack Down, with a scrollbar indicating more items (the three container actions) and further categories (Updates, Security, Upkeep, Backups) below. Beneath the list, still visible, are the Schedule section's Simple/Advanced toggle, a Once/Hourly/Daily/Weekly/Monthly frequency row set to Daily, Time inputs reading 03:00, the generated preview 'At 03:00 AM · 0 3 * * *', the Enabled toggle, the Delete after successful run checkbox, and Cancel/Create buttons." />
</Frame>
Common fields:
@@ -90,7 +90,7 @@ Common fields:
Conditional fields per action:
- **Stack actions** (Backup Stack Compose Files, Start / Bring Up Stack, Restart Stack, Auto-update Stack, Stop Stack, Take Stack Down) add a **Node** combobox and a **Stack** combobox. Restart Stack additionally renders a **Services** checkbox grid sourced from the stack's compose services on the selected node, so you can scope the restart to a subset instead of restarting the entire stack.
- **Stack actions** (Backup Stack Compose Files, Start / Bring Up Stack, Restart Stack, Auto-update Stack, Stop Stack, Take Stack Down) add a **Node** combobox and a **Stack** combobox. When the selected stack's compose file defines more than one service, Restart Stack additionally renders a **Services** checkbox grid so you can scope the restart to a subset instead of the entire stack; single-service stacks skip the grid since there is nothing to scope.
- **Container actions** (Restart Container, Stop Container, Start Container) add a **Node** combobox and a **Container** combobox listing every container on that node (running and stopped). The picker shows each container's name, state, and image. When the container is not part of a Sencho stack, helper text explains that the schedule targets the container by node and name.
- **Auto-update All Stacks on Node** adds a **Node** combobox. The helper text "Checks every stack on the selected node and updates stacks with newer images" appears above, next to the Runtime change badge.
- **Scan Node Images** adds a **Node** combobox listing local nodes only. The helper text "Runs Trivy against images on the selected local node and records the findings" and Read-only badge appear above.
@@ -98,7 +98,7 @@ Conditional fields per action:
- **Create Fleet Snapshot** shows a read-only **Scope: Entire fleet** summary instead of a Node or Stack picker, because it captures every node.
<Frame>
<img src="/images/scheduled-operations/create-restart.png" alt="The New scheduled task modal configured for a Restart Stack task. Name reads 'Nightly staging restart'. Action is Restart Stack. Node is Local. Stack is 'audit-mesh-prod'. A Services group with helper text '(leave empty for all)' shows two unchecked checkboxes labelled 'echo' and 'prober'. Cron Expression is '0 3 * * *' with the preview 'At 03:00 AM'. The Enabled toggle reads ON. The Delete after successful run checkbox is unchecked. Cancel and Create buttons sit at the bottom right." />
<img src="/images/scheduled-operations/create-restart.png" alt="The New scheduled task modal configured for a Restart Stack task. Name reads 'Nightly media restart'. Action is Restart Stack with an Interruptive risk badge and its helper text. Node is Local. Stack is 'swag'. No Services grid appears because the selected stack defines only one compose service. The Schedule preview reads 'At 03:00 AM · 0 3 * * *'. The Enabled toggle reads ON. The Delete after successful run checkbox is unchecked. Cancel and Create buttons sit at the bottom right." />
</Frame>
### Simple and Advanced modes
@@ -125,7 +125,7 @@ Because cron has no year field, a one-time schedule is stored as a fully pinned
### Per-service restart
When creating a Restart Stack task and a stack is selected, Sencho reads the compose file and renders a checkbox per defined service under **Services (leave empty for all)**. Check one or more boxes to restart only those services. Leave them all unchecked to restart the entire stack.
When creating a Restart Stack task and a stack is selected, Sencho reads the compose file. If it defines more than one service, a checkbox per service appears under **Services (leave empty for all)**; check one or more boxes to restart only those services, or leave them all unchecked to restart the entire stack. A stack with only one service skips the grid entirely, since there is nothing to scope.
### Scheduled vulnerability scans
@@ -134,7 +134,7 @@ A Scan Node Images task runs Trivy against every image on the selected node and
When a scheduled scan finishes, Sencho dispatches a completion notification. The severity reflects the outcome (info on a clean run, warning when findings are present); the category is `scan_finding`. The full message format and how it surfaces in the bell is documented in [Alerts & Notifications · Vulnerability scanning](/features/alerts-notifications#vulnerability-scanning).
<Frame>
<img src="/images/scheduled-operations/create-scan.png" alt="The New scheduled task modal configured for a Scan Node Images task. Name reads 'Nightly vulnerability scan'. Action is Scan Node Images. Node is Local. A Read-only risk badge and the helper text 'Runs Trivy against images on the selected local node and records the findings.' appear below the action picker. Cron Expression is '0 3 * * *' with the preview 'At 03:00 AM'. The Enabled toggle reads ON. Cancel and Create buttons sit at the bottom right." />
<img src="/images/scheduled-operations/create-scan.png" alt="The New scheduled task modal configured for a Scan Node Images task. Name reads 'Nightly vulnerability scan'. Action is Scan Node Images with a Read-only risk badge and the helper text 'Runs Trivy against images on the selected local node and records the findings.' Node is Local (the only option, since scans run on local nodes only). The Schedule preview reads 'At 03:00 AM · 0 3 * * *'. The Enabled toggle reads ON. Cancel and Create buttons sit at the bottom right." />
</Frame>
### Prune label filter
@@ -142,7 +142,7 @@ When a scheduled scan finishes, Sencho dispatches a completion notification. The
When creating a Prune Node Resources task, the **Label Filter (optional)** input scopes the prune to resources matching a specific Docker label. Use `key=value` format (for example, `com.docker.compose.project=staging`). Leave the field empty to prune every unused resource of the selected types.
<Frame>
<img src="/images/scheduled-operations/create-prune.png" alt="The New scheduled task modal configured for a Prune Node Resources. Name reads 'Weekly cleanup'. Action is Prune Node Resources. Node is Local. The Prune Targets group shows four checked boxes: Containers, Images, Networks, Volumes. The Label Filter (optional) input reads 'com.docker.compose.project=staging' with the helper text 'Only prune resources matching this Docker label.' Cron Expression is '0 4 * * 0' with the preview 'At 04:00 AM, only on Sunday'. The Enabled toggle reads ON. The Delete after successful run checkbox is unchecked. Cancel and Create buttons sit at the bottom right." />
<img src="/images/scheduled-operations/create-prune.png" alt="The New scheduled task modal configured for a Prune Node Resources task. Name reads 'Weekly cleanup'. Action is Prune Node Resources with a Destructive risk badge. Node is Local. The Prune Targets group shows four checked boxes: Containers, Images, Networks, Volumes. The Label Filter (optional) input reads 'com.docker.compose.project=staging' with the helper text 'Only prune resources matching this Docker label.' The Schedule is set to Weekly with Sunday selected and Time 03:00, showing the preview 'At 03:00 AM, only on Sunday · 0 3 * * 0'. The Enabled toggle reads ON. Cancel and Create buttons sit at the bottom right." />
</Frame>
### Stack lifecycle scheduling
@@ -229,7 +229,7 @@ Configure delivery channels in **Settings · Notifications · Channels**.
The Execution history button on any row opens a right-side sheet with the breadcrumb **Schedules `<task name>` Runs**. The sheet header reports the total run count, and a **Download CSV** secondary action exports the full history. The footer reads **Next run `<timestamp>`** when one is scheduled.
<Frame>
<img src="/images/scheduled-operations/run-history.png" alt="The Execution history sheet for a task named 'Vul Scan'. Breadcrumb reads 'Schedules VUL SCAN RUNS'. The italic display heading 'Vul Scan' sits above a '28 runs' meta line and a 'Download CSV' button. A table lists nineteen runs with columns Time, Source (every row a Scheduled badge), Status (every row a green Success badge), Duration (mixing fast cache-hit runs of 0.0s with full scans up to 21.9s), and Details (mixing 'Scanned 15 image(s). Found 27 critical…' rows with 'All 15 image(s) already scanned recently (cache hit)' rows). The footer reads 'NEXT RUN 5/15/2026, 3:00:00 AM'." />
<img src="/images/scheduled-operations/run-history.png" alt="The Execution history sheet for a task named 'Nightly Updates'. Breadcrumb reads 'Schedules NIGHTLY UPDATES RUNS'. The heading 'Nightly Updates' sits above a '15 runs' meta line and a 'Download CSV' button. A table lists runs with columns Time, Source (a mix of Scheduled and one Manual badge), Status (every row a green Success badge), Duration (ranging from about 6 seconds to over two minutes), and Details naming the stack updated and its outcome. The footer reads the next scheduled run time." />
</Frame>
The table columns are:
@@ -250,15 +250,15 @@ The CSV export includes every run in the history with columns: Timestamp, Source
A background Scheduler Service evaluates due tasks every 60 seconds. When a task's next run time has passed:
1. The scheduler checks that your license tier still allows the action; if not, the run is logged as failed and skipped.
2. It executes the configured action using the same internal services that power the equivalent UI buttons (Restart, Snapshot, Prune, and so on).
1. The scheduler checks that the target node still exists and is online; a deleted or offline node fails the run before anything executes.
2. It executes the configured action using the same internal services that power the equivalent UI buttons (Restart, Snapshot, Prune, and so on). Auto-update Stack and Start / Bring Up Stack additionally run through the same [deploy-enforcement](/features/deploy-enforcement) scan-policy gate as a manual deploy: if the stack's images violate an active policy, the run is blocked and recorded as a failure instead of deploying, and a warning notification names the policy and the offending images. Scheduled Auto-update Stack runs always take an atomic pre-update backup and roll back on failure, the same mechanism documented in [Atomic Deployments](/features/atomic-deployments), regardless of that feature's instance-wide default.
3. The result is logged to the execution history.
4. On failure, an `error`-level notification goes out through the configured channels. On recovery from a previously failing state, an `info`-level recovery notification is dispatched.
5. The next run time is recalculated from the cron expression.
Schedules are stored on the hub. Stack-targeted actions execute against the selected node, so lifecycle and per-stack update schedules can target either the hub or a connected remote node. Prune Node Resources and Scan Node Images run on local nodes only. Create Fleet Snapshot captures every node from the hub schedule.
Schedules are stored on the hub. Stack-targeted and container-targeted actions execute against the selected node, so lifecycle, per-stack update, and container schedules can target either the hub or a connected remote node. Prune Node Resources and Scan Node Images run on local nodes only. Create Fleet Snapshot captures every node from the hub schedule.
If a task is still running from a previous firing, the scheduler skips the new firing to prevent overlap.
If a task is still running from a previous firing, the scheduler skips the new firing to prevent overlap. The same protection also guards against collisions with other surfaces: if a stack already has a manual deploy, restart, or other lifecycle operation in flight when a scheduled task for that stack fires, the schedule skips its turn and the run is recorded as a failure naming the in-flight operation, rather than queuing behind it. The next scheduled tick tries again.
If Sencho restarts while a task is mid-execution, the orphaned run record is marked as failed with the message `Server restarted during execution` on the next startup. No manual cleanup is needed.
@@ -280,6 +280,12 @@ If Sencho restarts while a task is mid-execution, the orphaned run record is mar
<Accordion title="A Start / Bring Up Stack run fails with a compose file error">
Start / Bring Up Stack runs `docker compose up -d` against the stack's compose directory. If the stack folder has been removed or its compose file is missing, the run fails. Re-create or restore the stack from the [Editor](/features/editor), then click **Run now** on the schedule to confirm it succeeds before the next cron tick.
</Accordion>
<Accordion title="A scheduled Auto-update or Start / Bring Up Stack run was blocked by a policy">
Auto-update Stack and Start / Bring Up Stack run through the same [deploy-enforcement](/features/deploy-enforcement) scan-policy gate as a manual deploy. If the stack's current images match an active blocking policy, the run fails with a message naming the policy and the offending images, and a matching warning notification is dispatched. Review or adjust the policy, or update the flagged images, then click **Run now** to retry.
</Accordion>
<Accordion title="A scheduled task failed because another operation was already running">
Sencho serializes operations per stack. If a manual deploy, restart, or other lifecycle action is already in progress for a stack when its scheduled task fires, the schedule skips that turn and records a failure naming the operation that was already running, instead of queuing behind it. No action is needed: the task fires again at its next cron tick. Click **Run now** if you want to retry immediately once the other operation finishes.
</Accordion>
<Accordion title="A Backup Stack Compose Files run overwrote the previous backup">
Backup Stack Compose Files keeps a single slot per stack under `<DATA_DIR>/backups/<stack>/`. Each run overwrites the previous backup. This is by design: for a versioned archive of compose files across the fleet, use a [Create Fleet Snapshot](/features/fleet-backups) task, which retains every snapshot under the snapshots directory.
</Accordion>
+83 -14
View File
@@ -8,7 +8,12 @@ capabilities that power [vulnerability scanning](/features/vulnerability-scannin
center, so you can answer "what should I look at first?" without hunting through other pages. It is
scoped to the active node: the findings and scanner status you see reflect whichever node is selected.
The page is organized into tabs.
<Frame>
<img src="/images/security/overview.png" alt="Security page Overview tab with an Action needed masthead reading '3 actions: fixable findings, detected secrets' and CRITICAL/HIGH/LAST SCAN stat tiles, a Why Action needed review-queue card, a 30-day risk trend chart, an Action posture breakdown, and a Scan this node button." />
</Frame>
The page is organized into tabs: Overview, Images, Compose risks, Secrets, Policies, Suppressions,
History, and Scanner setup.
## Overview
@@ -27,6 +32,12 @@ posture itself: a vulnerable component being present is not the same as a reacha
The masthead carries a standing note to that effect, and posture weighs fix availability, exploit
intelligence, and triage decisions rather than raw severity alone.
An admin on a node with a ready scanner sees a **Scan this node** button above the review queue. It
opens a popover to pick any combination of image vulnerabilities, image secrets, and Compose
misconfigurations, then runs a node-wide scan in the [deploy progress](/features/deploy-progress)
modal; a partial failure across images or stacks surfaces as a warning toast rather than reading as
clean.
Below the masthead, a **Review queue** card leads the overview when actions or review items exist.
When the posture is Action needed the card is titled **Why Action needed** and lists each concrete
action with a count and a tab-shortcut button: fixable findings, known-exploited CVEs, detected
@@ -49,34 +60,62 @@ clear "overview unavailable" state and the other tabs keep working.
## Images
<Frame>
<img src="/images/security/images-tab.png" alt="Security page Images tab listing scanned images with Image, Findings, Last scan, Severity, and Actions columns; findings show critical/high counts and fixable totals, and one row shows a Clean badge." />
</Frame>
Image findings list every scanned image on the active node with its highest severity. Selecting an
image opens the full scan report, where you can review vulnerabilities, triage a CVE, and export an
SBOM. Each finding carries evidence tags so you can tell scary from exploitable at a glance:
known-exploited (KEV), EPSS exploitation probability, the CVSS score, and vendor "will not fix"
status, alongside whether a fix is available.
image opens the full scan report, where you can review vulnerabilities, triage a CVE, compare against
another scan, and export a CSV, a SARIF file, or an SBOM. Each finding carries evidence tags so you can
tell scary from exploitable at a glance: known-exploited (KEV), EPSS exploitation probability, the
CVSS score, and vendor "will not fix" status, alongside whether a fix is available.
<Frame>
<img src="/images/security/scan-report.png" alt="Vulnerability scan report sheet for nginx:latest showing 340 vulnerabilities, a Critical/High/Medium/Low summary, Compare/CSV/SARIF/SBOM export buttons, and a findings table with CVE, package, severity, installed version, and per-row EPSS and CVSS evidence tags." />
</Frame>
## Compose risks
<Frame>
<img src="/images/security/compose-risks.png" alt="Security page Compose risks tab in its empty state, with intro text describing privileged containers, Docker socket mounts, host networking, and broad capabilities, and a 'No Compose risks found' message prompting a node scan or per-stack config scan." />
</Frame>
Compose risks surface the security misconfigurations Trivy finds in your stack definitions rather than
image CVEs: privileged containers, Docker socket mounts, host networking, broad host bind mounts, public
database ports, and containers running as root. Each entry opens its scan report with the specific
findings and how to fix them. This is a security audit of the Compose file; for deploy-readiness checks
(port conflicts, missing bind paths, unset variables, no healthcheck), use
[Compose Doctor](/features/compose-doctor) from the stack page instead.
image CVEs, such as privileged containers, Docker socket mounts, host networking, or broad
capabilities. Each entry opens its scan report with the specific findings and how to fix them. This is
a security audit of the Compose file; for deploy-readiness checks (port conflicts, missing bind paths,
unset variables, no healthcheck), use [Compose Doctor](/features/compose-doctor) from the stack page
instead.
## Secrets
<Frame>
<img src="/images/security/secrets.png" alt="Security page Secrets tab listing an image with detected secret findings and a severity badge that opens the scan report's Secrets tab." />
</Frame>
The secrets tab lists images where Trivy detected exposed credentials or keys, and opens straight to
the secret findings for a scan.
## Policies
The Policies tab manages deploy-enforcement scan policies: severity thresholds that block or warn on a
deploy, scoped by stack pattern. See
[scan policies](/features/vulnerability-scanning#scan-policies) for the full configuration.
<Frame>
<img src="/images/security/add-policy.png" alt="New policy dialog with a Name field, an optional glob-style Stack pattern field, Block conditions toggles for Severity threshold, Known-exploited (KEV), and Fixable Critical/High, a Block on deploy toggle, and an Enabled toggle." />
</Frame>
The Policies tab manages deploy-enforcement scan policies: a policy names one or more block
conditions (a severity threshold, a known-exploited CVE, a fixable Critical or High finding), scoped
to a glob-style stack pattern or left blank to apply fleet-wide. Turning **Block on deploy** off keeps
a policy evaluating and alerting without rejecting the deploy. A separate **Honor suppressions in
deploy blocks** switch, off by default, decides whether a suppressed CVE still counts toward a block.
See [scan policies](/features/vulnerability-scanning#scan-policies) for the full configuration
reference.
## Suppressions
<Frame>
<img src="/images/security/suppressions.png" alt="Security page Suppressions tab with a CVE suppressions section (Export VEX and Add suppression buttons) and a Misconfig acknowledgements section (Add acknowledgement button), both showing empty states." />
</Frame>
CVE suppressions and misconfiguration acknowledgements are managed here, the same controls available
in Settings. These are governed by the local instance, so this tab is shown when you are on the local
node; switch to the local node to manage them.
@@ -88,10 +127,23 @@ export the fleet's triage decisions as an OpenVEX document for use with other to
## History
The History tab lists completed scans for the active node in an inline table. Prefer digest identity when a digest is stored (short digest as the primary label, image reference as the subtitle); config and other scans without a digest show the image reference. Search matches image references or digests. Select two scans to compare. The **Scan history** button in the [Resources Hub](/features/resources) is a shortcut to the same place. For how tags and digests relate, see [Tags vs digests](/features/vulnerability-scanning#tags-vs-digests).
<Frame>
<img src="/images/security/history.png" alt="Security page History tab listing completed scans with a short digest as the primary label and image reference as the subtitle, plus Last scanned, Trigger, Severity, Findings, Fixable, and Action columns, a search box, and a Compare (0/2) button." />
</Frame>
The History tab lists completed scans for the active node in an inline table. Prefer digest identity
when a digest is stored (short digest as the primary label, image reference as the subtitle); config
and other scans without a digest show the image reference. Search matches image references or
digests. Select two scans to compare. The **Scan history** button in the
[Resources Hub](/features/resources) is a shortcut to the same place. For how tags and digests relate,
see [Tags vs digests](/features/vulnerability-scanning#tags-vs-digests).
## Scanner setup
<Frame>
<img src="/images/security/scanner-setup.png" alt="Security page Scanner setup tab showing an Installed (managed) Vulnerability Scanner card at version v0.72.0 with an Uninstall link, and three toggles: Auto-update Trivy (on), Pre-deploy scan advisory (off), and Exploit intelligence KEV plus EPSS (on)." />
</Frame>
Scanner setup manages the Trivy binary for the active node: install the managed binary, update it when
a new release is available, and toggle automatic updates. Trivy is installed independently on each
node. See [Installing Trivy](/operations/trivy-setup) for the full setup options.
@@ -100,6 +152,23 @@ It also toggles **exploit intelligence**: a daily background fetch of the CISA K
EPSS scores that powers the known-exploited and EPSS evidence tags. It degrades gracefully when
offline and can be turned off for air-gapped or firewalled hosts.
A third, admin-only toggle, off by default, enables the **pre-deploy scan advisory**: an opt-in review
step that shows each image's latest scan severity before a manual deploy runs from the editor. It is
visibility only and never blocks a deploy on its own; see
[Pre-deploy scan advisory](/features/vulnerability-scanning#pre-deploy-scan-advisory) for how it
behaves and how it differs from a blocking policy on the Policies tab.
## On a phone
<Frame>
<img src="/images/security/mobile.png" alt="Mobile Security page with a masthead-led SECURITY kicker, an Action needed state word, a horizontally scrollable tab strip, a full-width Scan this node button, CRITICAL/HIGH/LAST SCAN tiles, and the Why Action needed review queue." />
</Frame>
Security uses the bespoke mobile masthead pattern: no top bar, a scrollable tab strip mirroring the
desktop tab order, and the same panels underneath. The scanner-detections note moves into the
masthead's info affordance, and the node-scan launcher becomes a full-width button at the top of the
Overview panel.
## What stays where
Resources keeps its per-image severity badges and scan actions, so you can still scan and inspect an
+46 -19
View File
@@ -6,7 +6,7 @@ description: Manage, group, pin, and bulk-act on your stacks from the primary si
The stack sidebar is your command center for every stack on the active node. Stacks are grouped by label, frequently used stacks can be pinned to the top, search fans out across all connected remote nodes, bulk mode lets you act on several stacks at once, and an activity ticker at the bottom surfaces ongoing operations and recent events.
<Frame>
<img src="/images/sidebar/sidebar-overview.png" alt="Stack sidebar showing the Sencho logo with version tag, the NODE LOCAL node switcher, Create Stack button plus bulk mode and scan-folder icons, the Search stacks input, ALL/UP/DOWN/UPDATES filter chips, a starred PINNED group at top, label groups MEDIA and UTILITIES with stack rows each showing a status pill and label dots, and the activity ticker at the bottom reading LIVE VIEW STACK" />
<img src="/images/sidebar/sidebar-overview.png" alt="Stack sidebar showing the Sencho logo with version tag, the NODE LOCAL node switcher, Create Stack button plus bulk mode and scan-folder icons, the Search stacks input, ALL/UP/DOWN/UPDATES filter chips, label groups MEDIA, UTILITIES, NETWORK, DATABASE, and UNLABELED with stack rows showing status pills and fuchsia update dots, and the activity ticker at the bottom reading LIVE VIEW STACK" />
</Frame>
## Layout
@@ -28,33 +28,41 @@ Four chips sit below the search box. Each shows a live count to the right of its
- **All**: every stack on the node.
- **Up**: stacks that are running with nothing crashed. A stack whose only stopped container finished cleanly (an init job that exited without error) still counts as up.
- **Down**: stacks that need attention, whether fully stopped or running with at least one crashed container (the `PT` state described below).
- **Updates**: stacks with at least one image update available. The chip renders in orange when the count is non-zero so you can spot pending updates at a glance. The Updates chip and the trailing update indicators on stack rows are controlled by the Image Update Checks [sidebar setting](/reference/settings#image-update-checks---sidebar). When the setting is off, the chip and indicators are hidden.
- **Updates**: stacks with at least one image update available. The chip renders in fuchsia when the count is non-zero, matching the fuchsia update dot on stack rows, so you can spot pending updates at a glance. The Updates chip and the trailing update indicators on stack rows are controlled by the Image Update Checks [sidebar setting](/reference/settings#sidebar). When the setting is off, the chip and indicators are hidden.
Counts reflect your current search text as well as the active node: typing into **Search stacks...** narrows every chip's count to the matching stacks, not the node's full total.
<Frame>
<img src="/images/sidebar/sidebar-filter-chips.png" alt="Filter chip row showing ALL (15), Up (15), Down (0), and Updates (1) with the Updates chip highlighted in orange and a collapse toggle icon on the right" />
<img src="/images/sidebar/sidebar-filter-chips.png" alt="Filter chip row showing ALL (15), Up (15), Down (0), and Updates (3) with the Updates chip highlighted in fuchsia and a collapse toggle icon on the right" />
</Frame>
Click the **** icon on the right to collapse the chip row when you need more vertical room; it becomes a **+** icon you click to bring the chips back. The collapsed state is remembered in your browser.
## First run
When the active node has no stacks yet, the list area below the filter chips replaces the empty groups with a discovery prompt: it reads your compose directory and offers **Adopt existing files** for compose files it finds, or **New stack** and **Scan stacks folder** when there's nothing to adopt. See [Adopt existing files](/features/stack-management#adopt-existing-files) for the full compose-discovery workflow.
## Groups
Stacks are grouped by label. Groups are sorted by size (most populated first), then alphabetically. Stacks with multiple labels appear in every matching group so you always see the full fleet membership per label. Stacks without labels appear in an **Unlabeled** group at the bottom. Click a group header to collapse or expand it; the collapsed state is remembered per node.
Each label group header also carries a small kebab button for muting that label's notifications, visible to admins on nodes that support notification suppression. See [Stack Labels](/features/stack-labels#sidebar-grouping) for how grouping and label management work, and [Mute Rules](/features/alerts-notifications#mute-rules) for what the mute presets create.
## Cross-node search
When you type anything into **Search stacks...**, Sencho fans the query out to every connected remote node at the same time. Results from other nodes appear below the local list under an **Other nodes** section, separated by a dividing line.
<Frame>
<img src="/images/sidebar/sidebar-cross-node-search.png" alt="Sidebar with a search query showing local results in label groups, then a dividing line and an OTHER NODES section listing stacks from three remote nodes with their status pills and external-link arrows" />
<img src="/images/sidebar/sidebar-cross-node-search.png" alt="Sidebar with a search query showing local results in label groups, then a dividing line and an OTHER NODES section listing stacks from three remote nodes (Opsix, Pitt-Moba, SLX-Mars) with their status pills and external-link arrows" />
</Frame>
Each reachable node shows its matching stacks with a status pill and an external-link arrow. Clicking a remote result switches the active node to that instance and opens the stack immediately.
If one or more nodes did not respond during the search, a collapsible warning row appears showing how many nodes were unreachable. Expand it to see which nodes failed and why.
If one or more nodes did not respond during the search, a collapsible warning row appears showing how many nodes were unreachable. Expand it to see which nodes failed and why. Nodes Sencho already shows as offline are skipped before the search starts, so they never appear in the results or in this warning; check [Fleet](/features/fleet-view) if you expected to find a stack there.
## Pinning
Right-click a stack and choose **Pin to top**. Pinned stacks sit in a dedicated **★ PINNED** group at the very top of the list. Up to 10 stacks can be pinned per node; pinning an 11th evicts the oldest. Right-click a pinned stack and choose **Unpin** to remove it.
Right-click a stack and choose **Pin to top**. Pinned stacks sit in a dedicated **★ PINNED** group at the very top of the list. Up to 10 stacks can be pinned per node; pinning an 11th evicts the oldest and shows a confirmation toast reading **Pinned. Unpinned oldest (max 10).** Right-click a pinned stack and choose **Unpin** to remove it.
<Frame>
<img src="/images/sidebar/sidebar-pinned.png" alt="Sidebar with a starred PINNED group at the top containing plex, followed by the MEDIA group and other label groups below" />
@@ -65,16 +73,15 @@ Right-click a stack and choose **Pin to top**. Pinned stacks sit in a dedicated
Each row gives you everything you need to read the stack at a glance, in a fixed column order:
- **Status pill** on the left. Two uppercase letters in mono type, or a spinner while a lifecycle action is in flight. `UP` (green) means the stack is running with nothing crashed. `DN` (red) means the stack is stopped. `PT` (amber) means the stack is partially running: at least one container is up and at least one has crashed (exited with an error, died, or is restart-looping). Hover the `PT` pill to see the running/total container count, such as `3/5 running`. A stack whose only stopped container finished cleanly stays `UP`.
- **Stack name** in mono type, truncated with an ellipsis when the row gets tight.
- **Label dots** to the right of the name. Up to three colored dots represent the stack's labels. If a stack carries more than three labels, a **+N** counter appears for the extras.
- **Stack name** in mono type, truncated with an ellipsis when the row gets tight. Rows no longer render a per-row label swatch; a stack's labels are visible from the group it's sorted into (see [Groups](#groups) above) or from the row's **Labels** submenu.
- **Trailing indicator** in its own fixed-width slot at the far right of the name area, showing the highest-priority signal present:
- Pulsing orange dot: an image update is available. Hover for the update tooltip; when per-service status is known, the tooltip names the outdated service(s).
- Pulsing fuchsia dot: an image update is available. Hover for the "Update available" tooltip; when per-service status is known, the tooltip names the outdated service(s).
- Muted circle-alert icon: the last registry check failed (registry unreachable, missing credentials, or rate-limited). Hover to read the failure reason.
- Git branch icon: a Git source update is pending and no image update is available.
- **Hover kebab** on the right edge. On desktop, hover the row to reveal a vertical three-dot menu that opens the same actions as right-clicking. On touch viewports the kebab is always visible.
<Frame>
<img src="/images/sidebar/sidebar-row-anatomy.png" alt="Four stack rows in the MEDIA group: bazarr (UP, purple label dot), plex (UP, purple label dot), radarr (UP, one label dot followed by an orange pulsing update indicator), and seerr (UP, purple label dot)" />
<img src="/images/sidebar/sidebar-row-anatomy.png" alt="Four stack rows in the UTILITIES group: dozzle (UP, fuchsia pulsing update indicator), maintainerr (UP), profilarr (UP), and unpackerr (UP)" />
</Frame>
## Bulk mode
@@ -84,19 +91,19 @@ Click the **Bulk mode** icon next to **Create Stack** (or press <kbd>B</kbd>) to
- **Start**, **Stop**, **Restart** apply the action to every selected stack.
- **Update** pulls the latest images for every selected stack.
Click rows to toggle their selection. The toolbar header shows a running count. Click the **×** in the toolbar to clear the selection, and click the icon again (or press <kbd>B</kbd>) to leave bulk mode.
Click rows to toggle their selection, or press <kbd>Ctrl</kbd>+<kbd>A</kbd> (<kbd>Cmd</kbd>+<kbd>A</kbd> on macOS) to select every stack currently matching your search text and active filter chip. The toolbar header shows a running count. Click the **×** in the toolbar to clear the selection while staying in bulk mode. Click the **Bulk mode** icon again or press <kbd>B</kbd> to leave bulk mode, or press <kbd>Esc</kbd> to leave bulk mode and clear the selection in one step.
<Frame>
<img src="/images/sidebar/sidebar-bulk-mode.png" alt="Sidebar in bulk mode: the stacked-rows icon in the action row is highlighted in brand color, a sticky toolbar shows 2 selected with a clear X button and Start/Stop/Restart/Update action buttons, and every stack row has a checkbox visible with bazarr and sonarr checked" />
<img src="/images/sidebar/sidebar-bulk-mode.png" alt="Sidebar in bulk mode: the stacked-rows icon in the action row is highlighted in brand color, a sticky toolbar shows 2 selected with a clear X button and Start/Stop/Restart/Update action buttons, and every stack row has a checkbox visible with radarr and sonarr checked" />
</Frame>
## Context menu
Right-click any stack (or open the kebab that appears on hover) for its context menu. Items are grouped by purpose:
- **Inspect**: **Alerts**, **Auto-Heal**, **Check updates**, and **Open App** (only appears when the stack is running and exposes a browser-reachable port).
- **Organize**: **Labels** opens a submenu listing every label on the node with a checkmark next to each one already assigned to this stack. Click any label to toggle it. The submenu also offers **New label** for inline creation (when you have edit permission and the node is under its label limit) and **Manage labels...** to open the full label settings. Below the Labels entry: **Pin to top** or **Unpin**.
- **Lifecycle**: **Deploy** (when stopped), **Stop**, **Restart**, **Update** (when running), and **Schedule task** (admin only, opens the Schedules view with this stack pre-selected).
- **Inspect**: **Alerts**, **Auto-Heal**, **Check updates**, **Open App** (only appears when the stack is running and exposes a browser-reachable port), and **Mute** (admins only, on nodes that support notification suppression). Mute opens a submenu with **Mute notifications for this stack**, **Mute deploy success noise**, **Mute monitor alerts for this stack**, and **Manage stack mute rules**; see [Mute Rules](/features/alerts-notifications#mute-rules) for what each preset creates.
- **Organize**: **Labels** opens a submenu listing every label on the node with a checkmark next to each one already assigned to this stack. Click any label to toggle it. The submenu also offers **New label** for inline creation (when you have edit permission and the node is under its label limit), **Manage labels...** to open the full label settings, and (admins only, on nodes that support notification suppression) a **Mute label…** submenu for quieting any label's notifications. See [Stack Labels](/features/stack-labels) for the full label workflow. Below the Labels entry: **Pin to top** or **Unpin**.
- **Lifecycle**: **Deploy** (when stopped), **Stop**, **Restart**, **Update** (when running), **Take down** (stops the stack and removes its containers and compose-created networks while keeping compose files on disk; opens a confirmation), and **Schedule task** (admin only, opens the Schedules view with this stack pre-selected).
- **Destructive**: **Delete** (only shown when you have permission to delete this stack).
<Note>
@@ -104,12 +111,12 @@ Right-click any stack (or open the kebab that appears on hover) for its context
</Note>
<Frame>
<img src="/images/sidebar/sidebar-context-menu.png" alt="Right-click context menu on a running stack with four labeled sections: INSPECT with Alerts, Auto-Heal, Check updates, and Open App; ORGANIZE with Labels (submenu arrow) and Pin to top; LIFECYCLE with Stop, Restart, Update, and Schedule task; and DESTRUCTIVE with Delete in red" />
<img src="/images/sidebar/sidebar-context-menu.png" alt="Right-click context menu on a running stack with four labeled sections: INSPECT with Alerts, Auto-Heal, Check updates, Open App, and Mute; ORGANIZE with Labels (submenu arrow) and Pin to top; LIFECYCLE with Stop, Restart, Update, Take down, and Schedule task; and DESTRUCTIVE with Delete in red" />
</Frame>
## Keyboard shortcuts
Shortcuts fire on the currently selected stack. They are blocked while a text input is focused or the global command palette is open.
Most shortcuts fire on the currently selected stack. The bulk mode shortcuts below are the exception and work without a selection. All shortcuts are blocked while a text input is focused or the global command palette is open.
### Lifecycle
@@ -132,7 +139,18 @@ On macOS, use <kbd>Cmd</kbd> in place of <kbd>Ctrl</kbd>.
| <kbd>H</kbd> | Open the auto-heal sheet |
| <kbd>U</kbd> | Check for image updates |
| <kbd>P</kbd> | Pin or unpin the stack |
### Bulk mode
These do not require a selected stack.
| Shortcut | Action |
|----------|--------|
| <kbd>B</kbd> | Toggle bulk mode |
| <kbd>Ctrl</kbd>+<kbd>A</kbd> | In bulk mode, select every stack matching the current search and filter chip |
| <kbd>Esc</kbd> | In bulk mode, exit and clear the selection |
On macOS, use <kbd>Cmd</kbd> in place of <kbd>Ctrl</kbd>.
## Activity ticker
@@ -162,10 +180,16 @@ Click the ticker in any other state to navigate directly to the linked view.
Confirm the active filter chip is **All** so a status filter is not hiding the row. Clear the **Search stacks...** box in case a stale query is filtering the list. Then click the **Scan stacks folder** icon next to **Create Stack** to re-index the compose directory on disk.
</Accordion>
<Accordion title="Pinning is not sticking past the 10th stack">
Each node has a 10-stack pin limit. When you pin an 11th stack, the oldest pin is automatically evicted so the new one fits. Unpin a stack you no longer need before adding another, or accept that the oldest will roll off.
Each node has a 10-stack pin limit. When you pin an 11th stack, the oldest pin is automatically evicted so the new one fits, and a toast confirms which one rolled off. Unpin a stack you no longer need before adding another, or accept that the oldest will roll off.
</Accordion>
<Accordion title="Keyboard shortcuts are not firing">
Shortcuts only fire when a stack row is selected in the sidebar (one row is highlighted). They are blocked while a text input is focused or the global command palette (<kbd>Ctrl</kbd>+<kbd>K</kbd>) is open. Click a stack row, then try the shortcut again.
Most shortcuts only fire when a stack row is selected in the sidebar (one row is highlighted); click a row and try again. <kbd>B</kbd>, <kbd>Ctrl</kbd>+<kbd>A</kbd>, and <kbd>Esc</kbd> are the exception and work without a selection. Every shortcut is blocked while a text input is focused or the global command palette (<kbd>Ctrl</kbd>+<kbd>K</kbd>) is open.
</Accordion>
<Accordion title="A stack row no longer shows its label as a colored dot">
Stack rows show only the status pill and name; per-row label dots were removed. To see or change a stack's labels, look at which group it's sorted into, or right-click the row (or use its kebab) and open the **Labels** submenu. See [Stack Labels](/features/stack-labels) for the full picture.
</Accordion>
<Accordion title="Mute is missing from a stack's context menu">
Mute (and the label group header's mute kebab) only render for admins on nodes that support notification suppression. Sign in with an admin account, or update the node if it's running a version that predates notification suppression.
</Accordion>
<Accordion title="The filter chips disappeared from the sidebar">
Click the **+** icon to the right of the search box to bring them back. The chip row collapses to a **** / **+** toggle, and the state is remembered in your browser, so an earlier collapse persists across reloads until you expand it again.
@@ -179,4 +203,7 @@ Click the ticker in any other state to navigate directly to the linked view.
<Accordion title="Cross-node search shows some nodes as unreachable">
Sencho tried to reach those nodes during the search but they did not respond. The node may be offline, its connection credentials may have expired, or a network change may have interrupted the link. Open the Fleet view to check each node's status, or go to Settings and test the connection from the Nodes panel.
</Accordion>
<Accordion title="A stack on a node I know is offline never shows up in search, and there's no unreachable warning for it either">
Nodes Sencho already shows as offline are skipped before the fan-out starts, so they're excluded from both the results and the unreachable warning rather than counted as a failed attempt. Reconnect the node first, then search again.
</Accordion>
</AccordionGroup>
+9 -6
View File
@@ -3,10 +3,10 @@ title: Stack Activity
description: Per-stack event log showing deploys, restarts, config changes, and alerts, each attributed to the user or system process that triggered them.
---
The **Activity** tab in the right-hand **Anatomy** panel gives you a timestamped record of everything that happened to the selected stack: deploys, restarts, stops, starts, image updates, drift events, and health gate outcomes. Every entry is attributed to the user or background process that triggered it, so you always have full operational context at a glance. The **files** and **edit** buttons on the same header strip belong to the panel as a whole and stay available regardless of which tab is active.
The **Activity** tab in the right-hand **Anatomy** panel gives you a timestamped record of everything that happened to the selected stack: deploys, restarts, stops, starts, take-downs, image updates, drift events, and health gate outcomes. Every entry is attributed to the user or background process that triggered it, so you always have full operational context at a glance. The **files** and **Edit compose** buttons on the same header strip belong to the panel as a whole and stay available regardless of which tab is active.
<Frame>
<img src="/images/stack-activity/activity-tab-populated.png" alt="Activity tab with the ACTIVITY tab selected in the header strip showing ANATOMY, ACTIVITY, DOSSIER, DRIFT, ENVIRONMENT, NETWORKING, DOCTOR, STORAGE tabs. Under a TODAY header, three events appear: 'plex deployed by admin' with a rocket icon 52m ago, 'plex updated by admin' with an up-arrow icon 12h ago, and 'plex update started by admin' with an up-arrow icon 12h ago." />
<img src="/images/stack-activity/activity-tab-populated.png" alt="Activity tab with the ACTIVITY tab selected in the header strip showing ANATOMY, ACTIVITY, DOSSIER, DRIFT, ENVIRONMENT, COMPOSE LABELS, NETWORKING, DOCTOR, STORAGE tabs. Under a YESTERDAY header, three events appear: 'plex updated by admin' with an up-arrow icon 17h ago, 'plex update started by admin' with an up-arrow icon 17h ago, and '[Node: Local] Stack &quot;plex&quot; has image updates available.' via Image Update 1d ago." />
</Frame>
## What you'll see
@@ -15,20 +15,23 @@ Each entry in the activity list contains four pieces of information:
| Field | Description |
|-------|-------------|
| **Icon** | Category of the event (deploy, restart, stop, start, image update, drift, and others) |
| **Icon** | Category of the event (deploy, restart, stop, start, take down, image update, drift, and others) |
| **Message** | What happened, for example `plex deployed`. Sensitive values such as tokens, secrets, basic-auth credentials, and bearer tokens are redacted before storage |
| **Attribution** | `by <username>` for user-initiated actions; `via <Subsystem>` for events triggered by background processes such as Auto-Heal or the Scheduler |
| **Time** | Relative time since the event: `just now`, `5m ago`, `2h ago`, `3d ago` |
If a [notification mute rule](/features/alerts-notifications#mute-rules) blocks delivery of a matching alert, the entry still appears here with a **Suppressed** badge next to the message. Hovering the badge shows which rule matched. Suppression only affects delivery to the bell and external channels; the activity row itself is never suppressed.
## Event categories
Ten categories have a dedicated icon. Any other notification scoped to a stack (deploy failures, scan findings, monitor alerts) also flows into the timeline and renders with a generic activity icon. Automatic creation of missing external networks during deploy writes a history-only activity row (`network_auto_created`) on the stack timeline; it does not appear in the notification bell.
Eleven categories have a dedicated icon. Any other notification scoped to a stack (deploy failures, scan findings, monitor alerts) also flows into the timeline and renders with a generic activity icon. Automatic creation of missing external networks during deploy writes a history-only activity row (`network_auto_created`) on the stack timeline; it does not appear in the notification bell.
| Category | Example message |
|----------|-----------------|
| Deploy succeeded | `plex deployed` |
| Stack restarted | `dozzle restarted` |
| Stack stopped | `radarr stopped` |
| Stack taken down | `radarr taken down` (or `radarr taken down (volumes removed)` when the take-down included volume removal) |
| Stack started | `radarr started` |
| Image update applied | `Auto-update: stack "dozzle" updated with new images` |
| Drift detected | `Drift detected on sonarr: 2 findings` |
@@ -72,7 +75,7 @@ Two states can make the tab look empty, and they mean different things:
Sencho attributes every recorded event to the actor that performed it:
- **User-initiated actions** (deploy, restart, stop, start, image update triggered from a button or the API): the actor is the authenticated username, shown as `by <username>`.
- **User-initiated actions** (deploy, restart, stop, start, take down, image update triggered from a button or the API): the actor is the authenticated username, shown as `by <username>`.
- **Background subsystems**: the actor is the subsystem name, shown as `via <Subsystem>`.
| Background subsystem | Displayed as |
@@ -120,6 +123,6 @@ Cleanup runs on the same schedule as container-metrics cleanup, so retention app
These are two distinct events. The first is a manual restart; the second is Auto-Heal restarting the container after a separate unhealthy-duration breach. Each event is its own entry with its own actor. Two rows here are correct behavior, not a duplicate.
</Accordion>
<Accordion title="Operational events don't appear in the notification bell dropdown">
The notification bell surfaces system alerts and error-level events. User-initiated operational events (start, stop, restart, deploy, update) appear exclusively in the per-stack Activity tab, keeping the global tray focused on conditions that need your attention.
The notification bell surfaces system alerts and error-level events. User-initiated operational events (start, stop, restart, take down, deploy, update) appear exclusively in the per-stack Activity tab, keeping the global tray focused on conditions that need your attention.
</Accordion>
</AccordionGroup>
+9 -6
View File
@@ -24,11 +24,11 @@ The top of the tab shows a read-only summary derived live from the stack's Compo
| Fact | What is shown | Source |
|------|--------------|--------|
| **Services** | Names of declared services | Compose file service keys |
| **Ports** | Count and all published host ports across all services | `ports:` entries, host-side bindings only |
| **Ports** | A count of every `ports:` entry declared across services, followed by each port number | `ports:` entries. The count is not filtered to host-published mappings: a container-only port (declared without a host side) is counted and listed the same way as a published one. |
| **Volumes** | Count of mounted volumes | `volumes:` references across services |
| **Network** | Stack's primary network name and type (for example, `arr-net · bridge`) | Compose network definitions |
| **Network** | Stack's primary network name, shown with a fixed `bridge` label (for example, `arr-net · bridge`) | Compose network definitions. The `bridge` label is fixed text, not read from the network's actual driver. |
| **Restart** | Restart policy of the first service with one set | `restart:` key |
| **Env file** | The env file path, its variable count, and names of any `${VAR}` with no supplied value | `.env` / `env_file:` at the stack root |
| **Env file** | The env file path, its variable count, and a count of any `${VAR}` with no supplied value. The Markdown export additionally lists the missing variable names; the live tab shows the count only. | `.env` / `env_file:` at the stack root |
| **Source** | `local`, or the Git remote URL, branch, and path when the stack is linked | Stack's git source configuration |
**Multi-file Compose stacks:** when a stack has a Git source with multiple Compose files, Sencho merges them (equivalent to `docker compose config`) before reading facts. Services, ports, and volumes shown in the Generated Facts reflect the merged result, not any individual file.
@@ -80,7 +80,7 @@ Two actions in the tab header produce a single Markdown document combining the g
- **copy md** copies the document to the clipboard.
- **download** saves it as `<stack-name>-dossier.md`.
The exported document always includes the Generated Facts and all Operator Notes. When the Networking tab capability is available on the active node, the export also includes a network exposure summary. When the Storage tab capability is available, the export also includes a storage summary. Both additions are automatic when the capability is present.
The exported document always includes the Generated Facts and all Operator Notes. When the active node advertises the [Compose Networking](/features/compose-networking) capability, the export also includes a network exposure summary; when it advertises the [Storage Portability](/features/compose-storage) capability, the export also includes a storage summary. Both additions are automatic when the capability is present, but each summary is fetched fresh at export time rather than cached, so a capable, up-to-date node can still omit a section if that fetch fails. See Troubleshooting below.
The export is clean Markdown that reads well in Git, Obsidian, BookStack, a README, or stored alongside a backup. The generated facts never include `.env` values, only variable names and counts. Your operator notes are exported exactly as you write them, so treat them like any document you might share and avoid pasting secrets you do not want in the export.
@@ -106,6 +106,10 @@ Fleet Snapshots can optionally capture dossier notes alongside Compose files. Wh
Events from the Drift tab (drift detected, drift resolved) appear in the stack's **Activity** timeline alongside deploys and restarts, giving a chronological view of when the stack drifted and when it recovered.
### Rollback readiness
At the bottom of the Dossier tab, on nodes that advertise the `update-guard` capability, a **Rollback readiness** section reports an overall **Ready**, **Partial**, or **Not ready** state for the stack, built from six signals: whether a compose backup exists, whether that backup covers the stack's env file, whether the image tag is pinned or moving, whether the last deploy succeeded, whether any service defines a healthcheck, and an explicit note that application data in named volumes and bind mounts is never restored by a rollback. It shares the same capability-plus-fetch-failure behavior described above for the Networking and Storage export summaries. See [Health-Gated Updates](/features/health-gated-updates#rollback-readiness) for what each signal means and how to act on it.
## Permissions and per-node scoping
Any authenticated user with `stack:read` permission can view a stack's dossier. Read-only users see all fields and the export buttons, but no Save button appears.
@@ -120,7 +124,6 @@ Dossiers are stored per stack and per node. A stack named `plex` on your home no
- **Documentation drift checks Access URLs only.** The port-validity check applies only to the Access URLs field. It does not validate other fields or check hostnames.
- **No history on operator notes.** The dossier stores only the current version of each field. Sencho does not record change history for operator notes. Use a version-controlled wiki or Git if you need note history.
- **Export requires a parseable Compose file.** When the Compose file cannot be parsed, the export buttons are disabled. Operator notes are unaffected and still save normally.
- **Rollback readiness requires a specific capability.** The rollback readiness section is shown only when the `update-guard` capability is active for the node.
## Accessing the Dossier tab
@@ -154,7 +157,7 @@ The Anatomy panel is visible in the right column when a stack is open and the ed
When a stack's compose file publishes a port through a variable such as `${PLEX_PORT}:32400`, Sencho does not know the resolved value from the Compose source, so it treats that port as unverifiable and stays quiet. If your access URL refers to the same port and a warning still appears, the URL's port does not match any statically declared published port. Either update the URL or pin the port in the Compose file.
</Accordion>
<Accordion title="The networking or storage summary is missing from the export">
The Markdown export includes a network exposure summary and a storage summary only when the corresponding capabilities are available on the active node. If those sections are missing, the capabilities are not enabled for that node. The core generated facts and all operator notes are always included regardless of capabilities.
A missing section has one of two causes. Either the active node does not currently advertise the [Compose Networking](/features/compose-networking) or [Storage Portability](/features/compose-storage) capability, because it is running an older Sencho build or was briefly unreachable when Sencho last checked, or the node does have the capability but the on-demand fetch for that summary failed at export time (a transient API, proxy, or Docker issue). Retry the export before assuming the node needs an upgrade. The core generated facts and all operator notes are always included regardless of either cause.
</Accordion>
<Accordion title="Dossier notes were not restored after a fleet snapshot restore">
Fleet snapshot restore has two steps: restoring Compose files and optionally restoring dossier notes. Notes are restored only when you explicitly choose to restore them at restore time. If you did not select that option, or if the original snapshot was captured without documentation enabled, the notes will not be present. To restore notes, choose a snapshot that includes documentation and confirm the notes restore option during the restore flow.
+14 -3
View File
@@ -81,6 +81,10 @@ If the Compose file declares a tag (`:latest`, `:1.25`) and the running containe
A Compose port range such as `8000-8002:8000-8002` is compared conservatively. Because the runtime expands ranges into individual ports, the comparison can read as a difference even when the deployment is correct. Sencho errs toward surfacing the possible difference rather than hiding it.
### Network findings and the Networking tab
The **network-undeclared** and **network-missing** findings reuse the same comparison the stack's [Networking](/features/compose-networking) tab uses, so the two surfaces never disagree about a network attachment. The difference is history: this tab persists findings in the drift ledger over time, while the Networking tab always shows the current live state with no history. A stack with an open network finding also counts toward the **Drift** chip in the Fleet Overview's Networking filter group, so a network-attachment mismatch is visible both per-stack here and across the fleet there.
## When drift is recorded
**Opening the tab** runs the spatial engine and displays the current state. It does not write anything to the ledger or update the history.
@@ -117,20 +121,24 @@ Drift events also appear in the stack's **Activity** tab alongside deploys, rest
<img
src="/images/stack-drift/drift-tab-location.png"
alt="The tab bar at the top of the Anatomy panel showing Anatomy, Activity, Dossier, Drift, Environment, Networking, Doctor, and Files tabs with Drift selected"
alt="The tab bar in the stack detail view showing Anatomy, Activity, Dossier, Drift, Environment, and Compose Labels tabs with Drift selected, and separate Files and Edit compose buttons to the right"
/>
1. Click any stack in the sidebar to open it.
2. In the right-hand Anatomy panel, switch to the **Drift** tab.
2. In the right-hand panel, switch to the **Drift** tab. If the tab row doesn't fit the panel width, scroll it to reveal Drift alongside the stack's other tabs (Anatomy, Activity, Dossier, Environment, Networking, and so on).
3. Read the status badge and any findings.
4. Click **re-check** after editing the Compose file or after a manual Docker operation to refresh the ledger.
On mobile, the same report appears under the **Compose** section of the stack detail view.
**Files** and **Edit compose** are separate buttons next to the tab row, not tabs themselves; they open the file browser and the compose editor respectively.
On mobile, the same report appears under the **Compose** segment of the stack detail view.
## Requirements
Drift detection is available on all plans with no additional configuration. As long as the stack has a Compose file and Docker is reachable on the node, the tab is functional.
Viewing the tab requires read access to the stack; any role that can see a stack (including read-only **viewer** and **auditor** roles) can open the Drift tab. Clicking **re-check** requires the same read-level access, so a read-only role can still trigger a ledger write (new findings recorded, resolved findings cleared, an Activity event logged) even though it cannot edit or deploy the stack.
## Limitations
**Read-only and advisory.** Drift detection never alters a stack. It reports differences and records them in the ledger, but deploying to resolve drift is a separate, explicit action.
@@ -186,4 +194,7 @@ Drift detection is available on all plans with no additional configuration. As l
<Card title="Blueprint Model" icon="drafting-compass" href="/features/blueprint-model">
Blueprints have a separate policy-based drift mode (observe, suggest, enforce) distinct from the runtime drift detection described on this page.
</Card>
<Card title="Compose Networking" icon="diagram-project" href="/features/compose-networking">
The Networking tab's runtime drift section uses the same network comparison as the findings on this page, without the persisted history.
</Card>
</CardGroup>
+34 -24
View File
@@ -3,23 +3,23 @@ title: Files & Volumes
description: Browse and safely edit the config files inside a stack's mounted volumes, plus the files in the stack's own directory, from the dashboard.
---
Files & Volumes gives you direct access to the files your containers actually read at runtime: the configuration inside a stack's **mounted volumes** (bind mounts and named Docker volumes), as well as the files in the stack's own source directory. A typical use is editing the Nginx and proxy configuration inside a reverse proxy's `/config` volume without leaving the dashboard. It lives on the **Files & Volumes** tab inside the stack editor, alongside the dedicated `compose.yaml` and `.env` editors.
Files & Volumes gives you direct access to the files your containers actually read at runtime: the configuration inside a stack's **mounted volumes** (bind mounts and named Docker volumes), as well as the files in the stack's own source directory. A typical use is editing the Nginx and proxy configuration inside a reverse proxy's `/config` volume without leaving the dashboard. It lives on the **Files** tab inside the stack editor, alongside the dedicated `compose.yaml` and `.env` editors.
<Frame>
<img src="/images/stack-file-explorer/overview.png" alt="Files & Volumes tab open with the root switcher and a populated tree on the left and a config file open in the editor on the right" />
<img src="/images/stack-file-explorer/overview.png" alt="Files tab open with a populated nginx configuration tree on the left and default.conf open in the editor on the right" />
</Frame>
<Note>
Browsing is limited to the roots the stack itself declares: its source directory and the volumes mounted by its services. You cannot browse other stacks or arbitrary locations on the host.
</Note>
## Opening Files & Volumes
## Opening the Files tab
1. Click any stack in the left sidebar.
2. Click **edit** in the right-hand Anatomy panel header to open the editor.
3. Switch to the **Files & Volumes** tab.
2. Click **Edit compose** in the right-hand Anatomy panel header to open the editor.
3. Switch to the **Files** tab.
The **files** shortcut button next to **edit** in the Anatomy header opens the editor and selects the Files & Volumes tab in one click.
The **Files** shortcut button next to **Edit compose** opens the editor with the **Files** tab already selected. The shortcut only appears when your account has **stack read** permission on the stack; without it, the tab is not offered.
Files & Volumes is available on every Sencho tier. Read actions (browse, preview, download, inspect permissions) require **stack read** permission, which every signed-in role has by default. Write actions (upload, edit, create, rename, move, change permissions, delete) require **stack edit** permission on your account.
@@ -40,12 +40,13 @@ The same source mounted by more than one service is shown as a single root, summ
### When a volume cannot be browsed
A volume root is shown with a short explanation and is not browsable when:
A volume root shows a short explanation in place of its tree and is not browsable when:
- the bind mount points at a host path Sencho cannot reach (mount that path into the Sencho container to browse it),
- the mount targets a protected host location such as `/`, `/etc`, `/proc`, `/sys`, `/dev`, or the Docker socket,
- the mount overlaps Sencho's own managed stack area (browse the owning stack's source instead), or
- a named volume cannot be resolved on the node that owns the stack.
- the mount targets a single file rather than a directory (a bind that maps one file straight into the container has nothing to open a tree on),
- the mount targets a protected host location: the filesystem root, kernel/OS state (`/etc`, `/proc`, `/sys`, `/dev`, `/run`, `/var/run`), the base-image binaries and libraries Sencho's own runtime depends on (`/usr`, `/bin`, `/sbin`, `/lib`, `/lib64`, `/boot`, `/root`), or the Docker socket,
- the mount overlaps an area Sencho manages at runtime: its own application directory, its data directory, the OS temp directory, or a configured upload/scanner directory (browse the owning stack's source instead), or
- a named volume cannot be resolved: either Sencho could not find it on the node that owns the stack, or it could not reach Docker to check (a transient daemon or proxy issue, distinct from the volume simply not existing).
### Named-volume editing details
@@ -53,10 +54,12 @@ Named-volume editing is best-effort and bound by file ownership. The helper cont
## Layout
The Files & Volumes tab splits into two panes. The left pane holds the Browsing selector, the upload affordance, the **New file** and **New folder** buttons, and the directory tree for the selected root. The right pane is the action bar plus the file viewer.
The Files tab splits into two panes. The left pane holds the Browsing selector, the upload affordance, the **New file** and **New folder** buttons, and the directory tree for the selected root. The right pane is the action bar plus the file viewer.
A full-screen toggle sits next to **Close editor** whenever the Files tab is active. It collapses the identity, health, and log column on the left of the stack workspace so the tree and viewer use the full width, which helps when you are working with wide config files or a deep directory tree. Click it again (or switch to a different tab) to return to the normal two-column layout.
<Frame>
<img src="/images/stack-file-explorer/layout-panes.png" alt="Files tab two-pane layout showing the upload widget and tree on the left and the file viewer on the right" />
<img src="/images/stack-file-explorer/full-screen-mode.png" alt="Files tab in full-screen mode, with the Command Center column collapsed so the tree and viewer fill the width" />
</Frame>
## Browsing the directory tree
@@ -65,7 +68,7 @@ Folders sort before files, and entries within each group sort alphabetically.
Click a folder to expand or collapse it. Click a file to open it in the viewer on the right. Symlinks render with a chain icon and behave like files when clicked. Deleting a symlink removes only the link entry; the file it points to is untouched.
The tree is fully keyboard navigable. Tab into it and use the arrow keys to move between rows: **Up** and **Down** move row to row, **Right** expands a folder (then steps into it), **Left** collapses it (or steps out to the parent), **Home** and **End** jump to the first and last visible row, and **Enter** opens a file or toggles a folder.
The tree is fully keyboard navigable. Tab into it and use the arrow keys to move between rows: **Up** and **Down** move row to row, **Right** expands a folder (then steps into it), **Left** collapses it (or steps out to the parent), **Home** and **End** jump to the first and last visible row, and **Enter** or **Space** opens a file or toggles a folder.
Each row is fully clickable across the pane, so right-clicking anywhere on a row (not just on its name) opens that entry's context menu. Long names are never truncated: the tree scrolls horizontally so you can read the full name.
@@ -78,7 +81,7 @@ Protection applies to the **Stack source** root. On a volume root a file named `
On the stack source root, the amber dot in the tree marks the five canonical stack files: `compose.yaml`, `compose.yml`, `docker-compose.yaml`, `docker-compose.yml`, and `.env`.
<Frame>
<img src="/images/stack-file-explorer/protected-tree-marker.png" alt="File tree with amber dot markers next to compose.yaml and .env" />
<img src="/images/stack-file-explorer/protected-tree-marker.png" alt="Stack source tree with an amber dot marker next to compose.yaml" />
</Frame>
Two behaviours follow from the marker:
@@ -86,9 +89,11 @@ Two behaviours follow from the marker:
- **Dedicated tab redirect.** Clicking `compose.yaml`, `compose.yml`, or `.env` jumps you to the matching **compose.yaml** or **.env** tab so the save-and-deploy controls stay in front of you. `docker-compose.yaml` and `docker-compose.yml` are still flagged as protected, but they open in the regular file viewer because they are not the canonical Sencho file.
- **Delete is blocked at the stack root.** The five canonical files at the stack root cannot be deleted through the explorer: the delete is rejected. Remove the whole stack via Stack Actions instead. A same-named file nested in a subdirectory is an ordinary file and can be deleted after the type-to-confirm step.
An override file such as `compose.override.yaml` or `docker-compose.override.yml` is not one of the five canonical names, so it carries no amber dot and no delete restriction: Sencho treats it as an ordinary file even at the stack root.
## Viewing files
When you click a text file, its contents appear in the editor on the right. The Monaco editor auto-detects the language from the extension.
When you click a text file, its contents appear in the editor on the right. The Monaco editor auto-detects the language from the extension. A **word wrap** toggle in the viewer toolbar wraps long lines instead of scrolling horizontally; it defaults on and your choice is remembered across files and sessions.
| File type | Behaviour |
|-----------|-----------|
@@ -103,14 +108,14 @@ When you have stack edit permission, the editor opens in write mode. The toolbar
When you do not, the toolbar shows a `Read-only` chip and the editor refuses input.
<Frame>
<img src="/images/stack-file-explorer/viewer-edit-mode.png" alt="Viewer toolbar showing the filename label and the Save button next to a YAML buffer" />
<img src="/images/stack-file-explorer/viewer-edit-mode.png" alt="Viewer toolbar showing nginx.conf with an unsaved edit and the Save button now active" />
</Frame>
Click **Save** to write the file to disk. If you have unsaved edits and click a different file in the tree, a confirmation dialog asks whether to discard them; cancelling keeps you on the current file.
Saves use optimistic concurrency: the editor remembers when the file was last loaded, and a save that targets a stale version returns a "file changed elsewhere" notice with the current server-side content. Your typed buffer is preserved so you can review the new version and reapply your edits before saving again.
Writes are atomic at the filesystem level. Sencho stages the new content into a sibling temporary file, fsyncs, and promotes it via rename, so a crash or power loss never leaves a half-written target on disk.
Writes to the stack source root and to bind-mount volumes are atomic at the filesystem level: Sencho stages the new content into a sibling temporary file, fsyncs, and promotes it via rename, so a crash or power loss never leaves a half-written target on disk. Named-volume writes go through the helper container instead and are not atomic; see [Named-volume editing details](#named-volume-editing-details) above.
<Warning>
Editing a file does not restart any containers. If your stack reads the file at runtime (for example, a config file mounted as a volume), restart the relevant service after saving so the container picks up the new content.
@@ -190,7 +195,9 @@ Select more than one entry to act on them together.
Bulk Move and Delete skip the protected stack files (`compose.yaml`, `.env`, and the rest) at the stack root, which are reported as kept rather than acted on; they can still be included in a Download.
Each item is processed independently. If some succeed and others fail (for example, a permission error on one file), Sencho reports how many succeeded and leaves the items that failed selected so you can see which they were and retry. Very large selections are capped: a Download that would exceed the archive's file-count or total-size limit is refused before it starts.
Each item is processed independently. If some succeed and others fail (for example, a permission error on one file), Sencho reports how many succeeded and leaves the items that failed selected so you can see which they were and retry.
**Selection and archive limits.** A bulk action accepts at most 100 selected paths per request; select fewer items and repeat the action for a larger batch. A bulk Download is also capped at 5000 files and 1 GiB of uncompressed content: a selection that would exceed either limit is refused before the archive starts building, so narrow the selection or download large folders in smaller batches.
## Permissions (chmod)
@@ -248,14 +255,14 @@ The Permissions dialog opens for everyone; only users with stack edit permission
## Troubleshooting
<AccordionGroup>
<Accordion title="Files & Volumes tab is empty or returns a 403">
The signed-in role lacks **stack read** permission for this stack. Ask an admin to grant the permission on your account, or sign in with a role that already has it.
<Accordion title="The Files tab is missing">
The signed-in role lacks **stack read** permission for this stack, so the tab is not offered; opening it directly (for example via a bookmarked link) redirects you back to the Anatomy tab. Ask an admin to grant the permission on your account, or sign in with a role that already has it.
</Accordion>
<Accordion title="No browsable stack volumes detected">
The stack declares no volume Sencho can reach, so the explorer opens on the stack source. This happens when the stack uses only anonymous or `tmpfs` mounts, when its bind mounts point at host paths that are not mounted into the Sencho container, or when its named volumes have not been created yet. Deploy the stack so its named volumes exist, or mount the relevant host path into the Sencho container, then reopen the tab.
</Accordion>
<Accordion title="A volume is shown but cannot be browsed">
The root carries a short explanation. A bind mount to a host path Sencho cannot reach needs that path mounted into the Sencho container. A mount that targets a protected host location (`/`, `/etc`, `/proc`, `/sys`, `/dev`, the Docker socket) or that overlaps Sencho's managed stack area is intentionally not browsable. A named volume that cannot be resolved on the owning node is not browsable until it exists.
The root carries a short explanation. A bind mount to a host path Sencho cannot reach needs that path mounted into the Sencho container. A bind that targets a single file rather than a directory has no tree to browse. A mount that targets a protected host location (the filesystem root, kernel/OS paths, Sencho's own runtime binaries, or the Docker socket) or that overlaps an area Sencho manages at runtime is intentionally not browsable. A named volume that cannot be resolved on the owning node is not browsable until it exists, or until a transient Docker connectivity issue clears.
</Accordion>
<Accordion title="Saving a file in a named volume returns a permission error">
Sencho writes to named volumes as an unprivileged helper user. A file owned by `root` or a specific application user may not be writable by that helper. Adjust the file's ownership or permissions on the host, or edit the file from a context that owns it, then try again.
@@ -269,8 +276,8 @@ The Permissions dialog opens for everyone; only users with stack edit permission
<Accordion title="Save returns 'file changed elsewhere'">
Another writer (a teammate, a deploy hook, or an out-of-band edit on the host) saved the file after you loaded it. Use the diff to compare your edits against the current version, then save again. Your typed buffer is kept so you do not have to retype your changes.
</Accordion>
<Accordion title="Upload returns DISK_FULL">
The disk hosting the stack's compose directory has no free space. Check the volume mounted as `COMPOSE_DIR` (defaults to `/app/compose` inside the Sencho container) and free up space, or move the volume to a larger disk.
<Accordion title="Upload fails with no obvious reason">
A full disk on the host backing the target directory surfaces as a generic write failure, not a dedicated error code. Check the volume mounted as `COMPOSE_DIR` (defaults to `/app/compose` inside the Sencho container) or the volume backing the target, free up space, or move it to a larger disk, then retry the upload.
</Accordion>
<Accordion title="File shows as 'Binary file detected' but it is a text file">
The binary detection heuristic flagged null bytes or a high proportion of non-printable characters in the file's first kilobytes. This can happen with certain encodings, non-UTF-8 byte sequences, or unusual line-ending mixes. Click **Open as text anyway** on the binary panel to re-fetch the file as UTF-8 text and edit it in the inline viewer. Files above the 2 MB preview limit still need a Download.
@@ -297,6 +304,9 @@ The Permissions dialog opens for everyone; only users with stack edit permission
Bulk delete and move process each item independently and report how many succeeded. The items that failed stay selected so you can see which they were; open one to find out why (a common cause is a permission error on a named-volume file, or a name collision at the destination), fix it, and retry on the still-selected items.
</Accordion>
<Accordion title="Bulk download is refused as too large">
A bulk download is packed into a single archive with a fixed cap on both the number of files and the total uncompressed size. A selection that would exceed either cap is refused before the download starts. Narrow the selection, or download large folders in smaller batches.
A bulk download is packed into a single archive capped at 5000 files and 1 GiB of uncompressed content. A selection that would exceed either limit is refused before the download starts. Narrow the selection, or download large folders in smaller batches.
</Accordion>
<Accordion title="A bulk action is rejected for having too many items">
Bulk actions (download, move, delete) accept at most 100 selected paths per request. Select fewer items, or repeat the action across a few smaller batches.
</Accordion>
</AccordionGroup>
+66 -24
View File
@@ -3,11 +3,15 @@ title: "Stack Labels"
description: "Per-node tags that group your stacks by purpose, surface them under collapsible headers in the sidebar, and enable cross-stack bulk actions across the fleet."
---
A **Stack Label** is a per-node tag (name plus color) applied to any stack. A labeled stack groups under that label's header in the sidebar instead of the flat list, and Fleet View can filter the overview by tag. Admins also get two label-driven fleet actions: stop every stack labeled `prod` across every node, or add a label to stacks across nodes in one shot.
A **Stack Label** is a per-node tag (name plus color) applied to any stack. A labeled stack groups under that label's header in the sidebar instead of the flat list, and Fleet View can filter the overview by tag. Admins also get two label-driven fleet actions (stop every stack labeled `prod` across every node, or add a label to stacks across nodes in one shot) plus one-click shortcuts for muting a label's notifications.
<Note>
Stack Labels are a different feature from **Compose Labels** (the raw `labels:` key-value metadata Docker Compose services declare) and from **Node Labels** (used for Blueprint placement). See [Docker Label Audit](/features/docker-label-audit) for the former; Stack Labels never read or write Compose-level labels.
</Note>
<img
src="/images/stack-labels/sidebar-grouping.png"
alt="Sidebar showing stacks grouped under four uppercase label headers (MEDIA 7, UTILITIES 4, NETWORK 3, DATABASE 1). Each group shows its stacks with a small colored dot on the trailing edge of each row."
alt="Sidebar showing stacks grouped under uppercase label headers (MEDIA, UTILITIES, NETWORK, DATABASE) with a count chip on each header, plus an UNLABELED group. Each row shows only its name and status pill; the group header carries a small kebab button for label mute actions."
/>
## What problem this solves
@@ -15,33 +19,35 @@ A **Stack Label** is a per-node tag (name plus color) applied to any stack. A la
A flat sidebar of fifteen same-level stacks forces a scan of every name. With Stack Labels:
- **Stacks group by purpose, not by alphabet.** Each label becomes a collapsible section header, sorted by stack count, so the busy buckets (your media stack, your network stack) sit at the top and rarely-touched ones can be folded away.
- **A glance is enough.** Each row carries up to three colored dots on its trailing edge, so a stack tagged with two purposes (for example `prod` and `media`) shows both colors.
- **Bulk operations stop being copy-paste.** Stop every `prod` stack across the fleet from one card. Re-tag eight stacks at once when a service moves between concerns. No scripting, no per-stack menu hunt.
- **Alert noise can be silenced per group.** Mute a label's notifications (all of them, external channels only, or just low-priority ones) from the same places you manage the label, without hand-writing a mute rule.
## Anatomy of a label
| Field | Rule |
|---|---|
| **Name** | 1 to 30 characters. Letters, digits, spaces, and hyphens only (`^[a-zA-Z0-9 -]+$`). Case-sensitive and unique per node. |
| **Color** | One of ten swatches: teal, blue, purple, rose, amber, green, orange, pink, cyan, slate. The color drives the dot on each row, the bullet on the group header, and the swatch in the Fleet View **Tags** filter. |
| **Color** | One of ten swatches: teal, blue, purple, rose, amber, green, orange, pink, cyan, slate. The color drives the bullet on the group header, the dot in the Settings panel and the Fleet View Stack drill-down, and the swatch in the Fleet View **Tags** filter. |
| **Scope** | Per-node. The same name can exist on two different nodes with two different colors; the fleet-stop card matches by name across nodes. |
| **Limit** | 50 labels per node. The **+ New label** button switches to **Limit reached** when you hit the cap. |
| **Limit** | 50 labels per node. The **New label** button switches to **Limit reached** when you hit the cap. |
## Where labels appear
### Sidebar grouping
The stack list is split into one collapsible section per label. Group headers render in uppercase mono with a count chip on the right (`MEDIA 7`). Order is fixed: a `★ PINNED` group first if any stacks are pinned, then label groups sorted by stack count descending and by label name ascending, then `UNLABELED` last for stacks that carry no label. A stack tagged with two labels appears in both groups. Search and the **All / Up / Down / Updates** filter chips above the list operate on rows inside whichever groups are expanded.
The stack list is split into one collapsible section per label. Group headers render in uppercase mono with a count chip on the right (`MEDIA 6`). Order is fixed: a `★ PINNED` group first if any stacks are pinned, then label groups sorted by stack count descending and by label name ascending, then `UNLABELED` last for stacks that carry no label. A stack tagged with two labels appears in both groups. Search and the **All / Up / Down / Updates** filter chips above the list operate on rows inside whichever groups are expanded.
Every row also carries up to three colored trailing dots that mirror the assigned labels. Beyond three, an additional `+N` counter appears next to the dots so the row never grows unbounded.
Stack rows themselves show only the status pill and name; they don't render a per-row label swatch. To see or change which labels a stack carries, right-click the row (or use its kebab) to open the **Labels** submenu, or check the Settings Labels panel. The [Fleet View](/features/fleet-view) Stack drill-down (a separate view, opened from a node card's **Stack details** button) does render colored **fleet label dots** next to each stack it lists.
Each label group header also carries a small kebab button for muting that label's notifications; see [Muting a label's notifications](#muting-a-labels-notifications) below.
### Fleet View tags filter
The [Fleet View](/features/fleet-view) overview toolbar carries a **Filters** popover with a **Tags** multi-select. The dropdown lists every label that exists on any node in the fleet, with each entry rendered as a colored dot plus the label name. Selecting one or more tags filters the node cards to nodes that contain at least one stack with that label.
The [Fleet View](/features/fleet-view) overview toolbar carries a **Filters** popover whose sections include a **Tags** multi-select alongside the node-level filters. The Tags dropdown lists every label that exists on any node in the fleet, with each entry rendered as a colored dot plus the label name. Selecting one or more tags filters the node cards to nodes that contain at least one stack with that label.
<img
src="/images/stack-labels/fleet-tags-filter.png"
alt="Fleet Overview Filters popover open. The popover shows four sections: Status, Type, Severity, and Tags. The Tags multi-select is expanded into a dropdown listing five labels (Database, Media, Network, Prod, Utilities), each prefixed by a small colored dot."
alt="Fleet Overview Filters popover open with the Tags section expanded into a dropdown listing five labels (Database, Media, Network, Prod, Utilities), each prefixed by a small colored dot."
/>
A label that exists on only one of four nodes still appears in the dropdown; selecting it resolves to that single node.
@@ -58,10 +64,10 @@ A label that exists on only one of four nodes still appears in the dropdown; sel
<img
src="/images/stack-labels/settings-labels.png"
alt="Settings page open on the Organization section with Labels selected in the sidebar. The main panel shows the heading Labels with a LABELS 5/50 stat in the top right. Five label rows are listed: Database (teal dot, 1 stack), Media (blue dot, 7 stacks), Network (amber dot, 3 stacks), Prod (amber dot, 0 stacks), Utilities (gray dot, 4 stacks). A cyan plus New label button sits in the top right."
alt="Settings page open on the Organization section with Labels selected in the sidebar. The main panel shows the heading Labels with a LABELS 5/50 stat in the top right. Five label rows are listed: Database (teal dot, 1 stack), Media (blue dot, 6 stacks), Network (amber dot, 3 stacks), Prod (amber dot, 0 stacks), Utilities (slate dot, 4 stacks). A New label button sits in the top right."
/>
Hover any row to reveal a **Pencil** edit icon and a destructive **Trash** icon on the trailing edge. The edit dialog shares its chrome with the create dialog: the kicker reads `LABELS · NEW` for a new label or `LABELS · EDIT` when you opened it from the pencil, the body has a single `Label name` input plus the ten color swatches, and the footer has **Cancel** and **Create** (or **Save**) buttons.
Hover any row to reveal a **Pencil** edit icon, a destructive **Trash** icon, and (for admins whose node supports label muting) a **bell-off** icon that jumps straight to a prefilled mute rule for that label. The edit dialog shares its chrome with the create dialog: the kicker reads `LABELS · NEW` for a new label or `LABELS · EDIT` when you opened it from the pencil, the body has a single `Label name` input plus the ten color swatches, and the footer has **Cancel** and **Create** (or **Save**) buttons.
<img
src="/images/stack-labels/create-label-dialog.png"
@@ -72,14 +78,14 @@ Deleting a label opens a destructive confirmation with the kicker `LABELS · DEL
### Create and assign inline from the stack menu
Right-clicking a stack in the sidebar (or using the three-dot kebab menu on its row) opens the same context menu under the **organize** group. Click **Labels** to open a submenu listing every label that exists on the active node, with a checkmark next to each one currently assigned to this stack. Clicking a label toggles the assignment immediately. The two trailing items handle creation and full management:
Right-clicking a stack in the sidebar (or using the three-dot kebab menu on its row) opens the same context menu under the **organize** group. Click **Labels** to open a submenu listing every label that exists on the active node, with a checkmark next to each one currently assigned to this stack. Clicking a label toggles the assignment immediately. Below a separator, two more items handle creation and full management, and a **Mute label…** entry (see below) can appear below a second separator:
- **New label** drops an inline form into the same submenu (text input with placeholder `Label name`, the ten color swatches, **Create** / **Cancel** buttons). Submitting creates the label on this node and assigns it to the stack in a single round trip. The entry hides itself once the node hits 50 labels.
- **Manage labels...** sends you to **Settings · Organization · Labels** for bulk renames, recolors, and deletions.
<img
src="/images/stack-labels/context-menu-labels.png"
alt="Stack context menu open on the Home page with the Labels submenu expanded. The submenu lists five labels: Database, Media (with a checkmark indicating it is assigned to this stack), Network, Prod, and Utilities, each with a colored dot. Below a separator are a plus New label entry and a Manage labels link."
alt="Stack context menu open on the Home page with the Labels submenu expanded. The submenu lists five labels: Database, Media (with a checkmark indicating it is assigned to this stack), Network, Prod, and Utilities, each with a colored dot. Below a separator are a plus New label entry and a Manage labels link, then below a second separator a Mute label entry."
/>
<img
@@ -87,7 +93,18 @@ Right-clicking a stack in the sidebar (or using the three-dot kebab menu on its
alt="The Labels submenu showing the inline create form after clicking New label. The submenu shows a focused Label name text input followed by a row of ten colored circles and a row of two buttons: Create (disabled while the name field is empty) and Cancel."
/>
A stack can carry multiple labels and will then appear under each label's group in the sidebar. There is no per-stack label cap; the only cap is the per-node total of 50.
The **Labels** entry itself only appears in the context menu for users with edit rights on that stack (**admin** or **node-admin**); a viewer or deployer won't see it, even though they can still see label grouping and dots elsewhere. A stack can carry multiple labels and will then appear under each label's group in the sidebar. There is no per-stack label cap; the only cap is the per-node total of 50.
## Muting a label's notifications
Every Labels surface gives admins a shortcut into [Mute Rules](/features/alerts-notifications#mute-rules) scoped to a label, so a noisy group of stacks can be quieted without hand-building a rule. This requires the **admin** role and the active node advertising the `notification-suppression` capability; the mute controls simply don't render otherwise.
Two patterns are used, both wired to the same underlying rule fields (label, and an **Apply to** target of Bell / External / Both):
- **Instant presets**, available from the sidebar's per-label group-header kebab and from the stack context menu's **Mute label…** submenu (pick a label, then a preset): **Mute notifications for this label** (mutes bell and external delivery), **Mute external alerts for this label** (external channels only, bell stays active), and **Mute low-priority stack alerts** (mutes `info` and `warning` severities for the label, leaving `error` alerts audible). Each fires immediately and confirms with a toast; no dialog to review.
- **Prefilled review**, available from **Manage label mute rules** (the same two menus) and from the bell-off icon on each row in **Settings · Organization · Labels**. Both open **Settings · Notifications · Mute Rules** with a **New mute rule** dialog already filled in for that label (name `Mute label: <name>`, the label pre-selected, **Both** as the target) so you can adjust node scope, categories, or severity before saving.
None of these delete or hide the underlying alerts; they only affect delivery to the bell, external channels, or both. See [Alerts & Notifications](/features/alerts-notifications#mute-rules) for the full Mute Rules field reference, suppression-vs-routing behavior, and how suppressed alerts still show up (marked **Suppressed**) in stack activity history.
## Fleet · Fleet Actions
@@ -99,27 +116,27 @@ Two cards in the **Fleet · Actions** tab use labels to drive cross-node operati
<img
src="/images/stack-labels/fleet-actions.png"
alt="Fleet Actions tab. The left column shows the bottom portion of the Prune fleet-wide card and below it the Stop by label card (kicker FLEET · ACTIONS · STOP BY LABEL) with a DESTRUCTIVE badge, DRY RUN and STOP FLEET buttons, and a label name input field. The right column shows the Bulk label assign card with five label pills (Database, Media, Network, Prod, Utilities) and a scrollable stack checklist grouped by node."
alt="Fleet Actions tab showing the Prune fleet-wide card (Maintenance badge, target checkboxes, per-node estimate) on the left, the Bulk label assign card (Transformative badge, five label pills Database/Media/Network/Prod/Utilities, a Target stacks checklist grouped by node, Reset/Apply buttons) on the right, and the top of the Stop by label card below."
/>
### Stop fleet by label
Type a stack label name into the input field. Sencho fans the request out to every node and stops every stack on that node assigned a stack label with the same name.
**Live preview.** As you type (debounced 500 ms), a preview lists which stacks on which nodes would be stopped. The **Stop fleet** button stays disabled until the preview resolves to at least one matching stack.
**Live preview.** As you type (debounced 500 ms), a suggestion listing the label's name, stack count, and node breakdown appears below the input, and a preview panel lists which stacks on which nodes would be stopped. The **Stop fleet** button stays disabled until the preview resolves to at least one matching stack.
<img
src="/images/stack-labels/fleet-stop-preview.png"
alt="Stop by label card with 'Media' typed in the label input. The badge reads 'DESTRUCTIVE · 7 STACKS · 1 NODES'. Below the input a preview panel shows the label Media with the entry 'Local · 7 stacks · 1 node', and beneath that a list of six stacks: bazarr, plex, radarr, seerr, sonarr, tautulli, each tagged UP and LOCAL."
alt="Stop by label card with 'Media' typed in the label input. A Destructive badge reads '6 stacks · 1 nodes'. A suggestion entry below the input reads 'Media · 6 stacks · 1 node · Local'. A preview panel below lists 6 stacks: plex, radarr, seerr, sonarr, tautulli, threadfin, each tagged UP and Local."
/>
**Dry run.** Click **Dry run** to simulate the operation without stopping any containers. The card shows the per-node breakdown labelled as a dry run and records the resolved target list. A completed dry run also unblocks the **Stop fleet** button when the live preview endpoint is unavailable.
This action targets stack labels only, never node labels. A confirmation modal shows the concrete list of nodes and stacks before the stop runs. A node with no matching label is noted as such; an unreachable node is reported but never blocks the stop on reachable nodes.
This action targets stack labels only, never node labels. A confirmation modal shows the concrete list of nodes and stacks before the stop runs, and the stop is bound to that exact confirmed list; nothing added to the label between confirming and executing gets swept in. A node with no matching label is noted as such; an unreachable node is reported but never blocks the stop on reachable nodes.
### Bulk label assign
Pick a label that exists anywhere in the fleet, then tick the stacks you want across one or more nodes (grouped by node, with a filter and per-node select-all).
Pick a label that exists anywhere in the fleet, then tick the stacks you want across one or more nodes (grouped by node, with a filter and per-node select-all). **Reset** clears the current label pick and stack selection; **Apply** stays disabled until at least one target stack is checked.
When the chosen label name exists with different colors on different nodes, the card shows a notice: "This label uses different colors on different nodes. The shown color is applied where it is created." Color resolution order is: the local node's color first, then the most common color across nodes, then the first seen.
@@ -127,11 +144,16 @@ When the chosen label name exists with different colors on different nodes, the
## Limits and rules
- **50 labels per node.** Settings hides the **+ New label** button at the cap; the inline `New label` entry in the stack menu hides itself too.
- **50 labels per node.** Settings hides the **New label** button at the cap; the inline `New label` entry in the stack menu hides itself too.
- **Names are unique per node**, case-sensitive. The same name on two nodes is two separate label rows. Cross-node fleet stop and bulk assign both match by name across nodes; each node resolves the name to its own label (and bulk assign creates it there if missing).
- **Allowed name characters**: letters, digits, spaces, and hyphens. Empty names and names beyond 30 characters are rejected at the API.
- **Bulk-action concurrency**: only one label-driven bulk action can run on a single node at a time. A second concurrent attempt against the same node returns HTTP 429 and the operator sees an error toast; the in-flight action keeps running.
- **Permissions**: Every signed-in user can view labels: sidebar grouping, trailing dots on stack rows, the Settings panel (read-only), and the Fleet View Tags filter all work for every user. Creating, renaming, recoloring, and deleting labels requires the **admin** or **node-admin** role. The label-based Fleet Actions (Stop fleet by label, Bulk label assign) require the **admin** role.
- **Bulk assign size cap**: a single Apply is capped at 1,000 total stack assignments across every selected node; the request is rejected past that so one accidental fleet-wide selection can't force an unbounded number of writes.
- **Permissions**: every signed-in user can view labels: sidebar grouping, the Settings panel (read-only), and the Fleet View Tags filter all work for every user. Creating, renaming, recoloring, deleting labels, and toggling a label's assignment on a stack (including from the context menu) requires the **admin** or **node-admin** role. The label-based Fleet Actions (Stop fleet by label, Bulk label assign) and the mute shortcuts require the **admin** role.
<Tip>
Beyond the UI, an admin can drive one label at a time on the current node through the API: `POST /api/labels/:id/action` with `{ "action": "deploy" | "stop" | "restart", "dryRun": true }` runs that action across every stack carrying the label, node-locked against other bulk operations the same way the fleet cards are. There's no button for this yet, so it's reachable only via a **Full Admin** API token or a direct request from an admin session; see [API Tokens](/features/api-tokens) for how to mint one.
</Tip>
## Troubleshooting
@@ -139,11 +161,11 @@ When the chosen label name exists with different colors on different nodes, the
<Accordion title="Stacks are not grouped under labels in the sidebar">
Grouping requires at least one stack to carry at least one label. With zero labels assigned the sidebar collapses into a single `UNLABELED` group, which is rendered as a flat list. Create a label from **Settings · Organization · Labels** or right-click any stack and use **Labels · New label**, assign it to a stack, and the grouped layout takes over.
</Accordion>
<Accordion title="The trailing colored dots are missing on stack rows">
A stack row only renders trailing dots when at least one label is assigned to that stack. Right-click the row, open the **Labels** submenu, and tick at least one label; the dots appear on the next sidebar refresh. If a stack already has labels assigned but the dots still do not appear, check that the active node is the one that owns the assignments. Labels are per-node, so switching the node switcher to a different instance shows that instance's assignments only.
<Accordion title="I don't see the Labels item in a stack's context menu">
The **Labels** submenu (both viewing checkmarks and toggling assignment) requires **admin** or **node-admin** rights on that stack. Viewers and deployers can still see the sidebar grouping and the read-only Settings panel, but the context menu won't offer a way to change labels.
</Accordion>
<Accordion title="New label is missing from the stack context menu">
The active node already has 50 labels (the per-node cap). Both the inline New label entry in the stack submenu and the + New label button in **Settings · Organization · Labels** hide themselves at the cap. Delete an unused label or rename an existing one to free a slot.
The active node already has 50 labels (the per-node cap). Both the inline New label entry in the stack submenu and the New label button in **Settings · Organization · Labels** hide themselves at the cap. Delete an unused label or rename an existing one to free a slot.
</Accordion>
<Accordion title="The Tags filter does not list a label I just created">
The Tags filter aggregates labels across every node in the fleet by name. If the new label only exists on one node and that node was offline at the moment the page loaded, the dropdown may not include it. Refresh **Fleet · Overview** with the toolbar refresh button to repull node state.
@@ -163,7 +185,27 @@ When the chosen label name exists with different colors on different nodes, the
<Accordion title="The Fleet Actions cards return an error when I click Apply or Stop fleet">
The label-based Fleet Actions require the admin role. Confirm the active user has the admin role under **Settings · Users**; all other roles see the cards but cannot execute them.
</Accordion>
<Accordion title="I don't see a Mute option on a label">
Label muting requires the **admin** role and the active node advertising the `notification-suppression` capability. Update the node if it's on an older version, or confirm your role under **Settings · Users**.
</Accordion>
<Accordion title="Deleting a label removed it from every stack">
Working as designed. The destructive confirmation reads `Removes the label from every stack across the fleet.` The label row and every assignment row that pointed to it are dropped in a single transaction. There is no undo; recreate the label by name and color and reassign the affected stacks if you need to recover.
</Accordion>
</AccordionGroup>
## Related features
<CardGroup cols={2}>
<Card title="Fleet view" icon="layout-grid" href="/features/fleet-view">
See fleet label dots per stack in the drill-down, and filter the whole fleet by tag.
</Card>
<Card title="Fleet Actions" icon="bolt" href="/features/fleet-actions">
The full reference for every cross-node action, including Stop by label and Bulk label assign.
</Card>
<Card title="Alerts & Notifications" icon="bell" href="/features/alerts-notifications">
The complete Mute Rules field reference and how suppression interacts with routing.
</Card>
<Card title="Docker Label Audit" icon="tags" href="/features/docker-label-audit">
Audit Compose-level and Node labels, the two label systems Stack Labels is deliberately kept separate from.
</Card>
</CardGroup>
+24 -16
View File
@@ -115,10 +115,10 @@ The image tag is taken from the final positional argument (for example `nginx:al
**Troubleshooting conversion:**
<Frame>
<img src="/images/stack-management/convert-tab-error.png" alt="Error toast shown above the New stack dialog when the converter cannot parse the input" />
<img src="/images/stack-management/convert-tab-error.png" alt="Error toast in the bottom-right corner reading 'Could not parse command' while the New stack dialog stays open" />
</Frame>
If the converter cannot produce a usable compose file, the request fails with a clear error toast. Common causes:
If the converter cannot produce a usable compose file, the request fails and a toast notification appears in the bottom-right corner. Common causes:
- **Not a docker run command.** The input must begin with `docker run` and include an image reference. Free-form text, `docker compose` commands, and shell pipelines are rejected.
- **Unrecognized flag.** The parser supports the flags listed above. Rare flags (such as `--userns`, `--ipc`, custom runtime options) are not recognized. Remove the flag, convert the rest, and add it back by hand in the editor.
@@ -178,7 +178,7 @@ Click any group header to collapse or expand it. The group's stack count appears
Opening a stack puts identity and state front and center.
<Frame>
<img src="/images/stack-view/identity-header.png" alt="Stack identity header showing breadcrumb, title, state pill, and action row" />
<img src="/images/stack-view/identity-header.png" alt="Stack identity header showing breadcrumb, title, state pill, and action row for the plex stack" />
</Frame>
The header answers two questions at a glance:
@@ -191,7 +191,7 @@ The header answers two questions at a glance:
- `running` (green) when no healthcheck is defined.
- `exited` (red) when no containers are up.
The action row below the title keeps everyday lifecycle controls visible. The **More actions** overflow holds Rollback, Scan config, and Delete; the next sections cover both surfaces.
The action row below the title keeps everyday lifecycle controls visible. The **More actions** overflow holds Rollback, Scan config, Mute, and Delete; the next sections cover both surfaces.
If Sencho discovers its own compose project as a stack (because the deployment directory lives inside `COMPOSE_DIR`), that stack is treated as the running Sencho instance: deploy, update, stop, down, and delete are disabled in the UI and refused by the API. Use **Fleet → Node Update** to upgrade Sencho. Compose Doctor flags the condition during preflight.
@@ -210,7 +210,7 @@ The same links button appears on update cards in [Auto-Update Policies](/feature
Below the header, each container in the stack gets a single row that answers "is this piece working, and how do I reach it?" When the stack has multiple containers, a summary strip appears above the list showing total, running, paused, and unhealthy counts, along with a **Compact / Detailed** toggle. Compact mode shows status, name, uptime, port, and action buttons; detailed mode (the default) adds CPU, memory, and network I/O sparklines.
<Frame>
<img src="/images/stack-view/containers.png" alt="CONTAINERS section showing a container card with health badge, uptime, port mapping, open link, and CPU, memory, and network stat tiles" />
<img src="/images/stack-view/containers.png" alt="Container card for the plex service showing health badge, uptime, port mapping, open link, action icons, and CPU, memory, and network stat tiles" />
</Frame>
Each row includes:
@@ -235,7 +235,7 @@ Single-service stacks keep the existing flat container layout; service headers d
The logs area at the bottom of the stack view has two modes. Toggle between them with the segmented control in the top-right of the panel; your choice is remembered for next time.
<Frame>
<img src="/images/stack-view/logs-viewer.png" alt="Logs panel in Structured mode with toolbar, level filter pills, and timestamped INFO log rows" />
<img src="/images/stack-view/logs-viewer.png" alt="Logs panel in Structured mode for the plex stack with toolbar, level filter pills, and timestamped INFO log rows" />
</Frame>
**Structured** (default) parses every line into a DOM row with three columns: local time, level badge, and message. Level detection is automatic:
@@ -256,10 +256,10 @@ The structured viewer holds up to 10,000 lines; older entries are dropped from t
## Anatomy panel
The right column of the stack view is a tabbed panel. Tabs appear only when the data they need is available.
The right column of the stack view is a tabbed panel: Anatomy, Activity, Dossier, Drift, Environment, Compose Labels, Networking, Doctor, and Storage. Tabs appear only when the data they need is available, and the row scrolls horizontally (with a chevron button) when there are more tabs than fit.
<Frame>
<img src="/images/stack-view/anatomy-panel.png" alt="Anatomy panel showing all eight tabs: Anatomy, Activity, Dossier, Drift, Environment, Networking, Doctor, and Storage, with the Networking tab active" />
<img src="/images/stack-view/anatomy-panel.png" alt="Anatomy panel with the Anatomy tab active, showing services, ports, volumes, restart, env_file, network, and source rows plus the tab row" />
</Frame>
The **Files** and **Edit compose** buttons sit to the right of the tab row.
@@ -281,10 +281,12 @@ Each row maps one compose concept to the value it resolves to right now:
A footer card under the rows surfaces the first published port as a clickable **EXPOSED** link, so you can jump straight to the running app.
If an image update is available, or the stack declares one or more services with a local `build:` section, an inline banner appears below the rows. Registry updates name each image with a pending update and show its version transition (`27.1.4 27.1.5`), risk classification (`safe · patch`, `minor`, or `major · review required`), and an **apply** button. Build-only stacks show **Rebuild available** with a **Rebuild & Update** button instead of a version bump. Mixed stacks (registry images plus local builds) show both signals. Major bumps show a rose banner and require explicit review before applying.
If an image update is available, or the stack declares one or more services with a local `build:` section, an inline banner appears below the rows. Registry updates name each image with a pending update and show its version transition (for example `27.1.4 -> 27.1.5`); the detail line below reads `patch · safe to apply` (green), `minor · review recommended` (amber), or `major · breaking changes possible` (rose), followed by an **apply** button. Build-only stacks show **Rebuild available** instead of a version bump, with a **Rebuild & Update** button. A stack that mixes registry images and local builds still shows a single banner, with the rebuild note folded into the same detail line. Major bumps use the rose styling and are worth reviewing before applying.
Rebuilds can take longer than a registry pull and depend on the local Dockerfile context, network access, and base-image availability. Atomic rollback restores compose and env files only; previously built image layers are not rolled back automatically.
A separate amber **scan** banner appears here if the most recent post-deploy vulnerability scan failed or was skipped for one or more images; deploys are never blocked by a scan failure. See [Vulnerability Scanning](/features/vulnerability-scanning).
### Activity
A timestamped audit trail of every deploy, restart, stop, update, and rollback on this stack, attributed to the user or subsystem that triggered it. See [Stack Activity](/features/stack-activity).
@@ -309,6 +311,10 @@ An inventory of every environment variable the stack uses, organized by status:
Variables that look like secrets (tokens, passwords, keys) are marked with a lock icon. When the stack references multiple env files, use the file picker in the tab header to switch between them.
### Compose Labels
Reconciles the Docker labels declared in the compose file against the labels present on each running container, per service, with a provenance badge (Compose file, Image, Runtime, or System) on every entry. These are Docker labels read by external automation such as Watchtower or Traefik, distinct from Sencho's own organizational Stack Labels (see [Stack Labels](/features/stack-labels)). See [Docker Label Audit](/features/docker-label-audit) for the full reconciliation model and the fleet-wide equivalent.
### Networking
Shows the stack's defined networks, external network attachments, and the driver for each, along with an exposure-intent classification per service. A Runtime Drift row confirms whether the running network topology matches what the compose file declares. See [Compose Networking](/features/compose-networking).
@@ -324,7 +330,7 @@ Runs preflight checks against the compose file and reports findings grouped by s
| **Warning** | Best-practice deviation worth reviewing |
| **Info** | Informational observation, no action required |
Each finding includes the affected service, a description, and a remediation suggestion. Click the refresh button to rerun the checks. See [Compose Doctor](/features/compose-doctor).
Each finding includes the affected service, a description, and a remediation suggestion. Click **run preflight** to rerun the checks. When the last run found blocker or high-risk findings, the **Doctor** tab label shows a small red or amber dot until you rerun preflight or dismiss the summary banner. See [Compose Doctor](/features/compose-doctor).
### Storage
@@ -362,6 +368,7 @@ The stack header groups actions by frequency of use. The most common action is t
| Secondary | **Update** | `docker compose pull` + `up -d` (or build-aware rebuild when services declare `build:`) | Pulls registry images and recreates containers. When one or more services use `build:`, Update rebuilds those images from source (`compose build --pull`), pulls any remaining registry images, then recreates containers. |
| Overflow | **Rollback** | Restores backup | Reverts compose and env files to the pre-deploy snapshot and redeploys. Only shown when a backup exists. |
| Overflow | **Scan config** | Trivy config scan | Scans the compose file for misconfigurations (admin role). |
| Overflow | **Mute** | Creates a mute rule | Quick presets to mute notifications, deploy-success noise, or monitor alerts for this stack, plus a link to manage its mute rules in full. See [Alerts and Notifications](/features/alerts-notifications). |
| Overflow | **Delete** | `down --volumes` + removes files | Stops and removes containers and volumes, then deletes the stack directory. |
**When stopped:**
@@ -433,24 +440,25 @@ Click the kebab on a stack row in the sidebar to open its context menu. The menu
- **Auto-Heal** (`H`): configure auto-recovery for this stack. See [Auto-Heal Policies](/features/auto-heal-policies).
- **Check updates** (`U`): force an image update check now.
- **Open App** (`↗`): open the stack's web interface in a new tab. Shown only when the stack is running and exposes a web port.
- **Mute** (``): quick presets to mute notifications, deploy-success noise, or monitor alerts for this stack, plus a link to manage its mute rules in full. See [Alerts and Notifications](/features/alerts-notifications).
**organize**
- **Labels** (``): assign or remove labels. See [Stack Labels](/features/stack-labels).
- **Pin to top** (`P`): pin or unpin the stack from the **PINNED** group at the top of the list.
- **Labels** (`L `): assign or remove labels. See [Stack Labels](/features/stack-labels).
- **Pin to top** (`P`): pins the stack to the **PINNED** group at the top of the list; toggles to **Unpin** once pinned.
**lifecycle**
- **Deploy** (`⌘↵`): shown when stopped, in place of Stop and Restart.
- **Stop** (`⌘.`): shown when running.
- **Take down** (`⌘↓`): shown when the stack has been deployed (running, partial, or exited). Opens a confirmation dialog; optional volume removal is offered when the active node supports it.
- **Restart** (`⌘R`): shown when running.
- **Update** (`⌘↑`): pulls the latest image tags and redeploys.
- **Schedule task**: open the scheduler pre-filled for this stack; pick **Auto-update Stack** to set up unattended image updates on your own cadence. See [Scheduled Operations](/features/scheduled-operations) and [Auto-Update Policies](/features/auto-update-policies).
- **Deploy** (`⌘↵`): shown when stopped, in place of Stop and Restart.
- **Take down** (`⌘↓`): shown when the stack has been deployed (running, partial, or exited). Opens a confirmation dialog; optional volume removal is offered when the active node supports it. Disabled for the stack running Sencho itself.
- **Schedule task**: admin only. Opens the scheduler pre-filled for this stack; pick **Auto-update Stack** to set up unattended image updates on your own cadence. See [Scheduled Operations](/features/scheduled-operations) and [Auto-Update Policies](/features/auto-update-policies).
**destructive**
- **Delete** (`⌘⌫`): admin only.
- **Delete** (`⌘⌫`): admin only. Disabled for the stack running Sencho itself.
Items are disabled while another operation is in progress on that stack.
+16 -2
View File
@@ -109,7 +109,13 @@ The mono-bracketed `[ Use authenticator ]` link toggles back to the six-digit TO
### Rate limiting
Five failed verifications in a row lock the account for 15 minutes. The challenge screen replaces the input with a `Retry in MM:SS` countdown and a `Rate limited` label while the lockout is in effect. The failure counter resets only on a successful sign-in: if you retry with another wrong code after the window expires, the counter is still at five and the lockout re-engages immediately. See [Lockout recovery](#lockout-recovery) below.
Five failed verifications in a row lock the account for 15 minutes. The whole verify screen switches to a throttled state: the kicker changes from `SENCHO · VERIFY` to `SENCHO · THROTTLED`, the large hero text becomes the live `MM:SS` countdown itself (replacing the word `Verify`), and the caption reads `Too many attempts. Take a breath and try again shortly.` Below that, a `Retry in` / `MM:SS` readout sits next to a `Rate limited` label. The failure counter resets only on a successful sign-in: if you retry with another wrong code after the window expires, the counter is still at five and the lockout re-engages immediately. See [Lockout recovery](#lockout-recovery) below.
Submitting the same TOTP code twice within its 30-second window (for example, a double-submit from a flaky connection) is rejected as a replay and **counts toward the five-attempt lockout counter** the same as a wrong code. This lockout counter and its 15-minute window apply only to the sign-in challenge. Repeated wrong codes on the enrolment confirm step, the Disable 2FA dialog, or the Regenerate backup codes dialog just return an error each time; they never lock the account.
<Note>
There is a second, separate limit in front of sign-in: Sencho also caps password and 2FA sign-in attempts from a single network address to 5 per 15 minutes. Because it is per-address rather than per-account, it is shared by everyone signing in from behind the same address (a shared office network, VPN egress, or NAT gateway). Once that shared budget is used up, sign-in attempts are rejected immediately with `Too many attempts. Please try again in 15 minutes.` for the rest of the window, for every account behind that address, regardless of the per-account MFA lockout described above.
</Note>
## Account panel anatomy
@@ -216,7 +222,9 @@ The reset bumps the target user's token version, so any active sessions of their
## Lockout recovery
The 15-minute lockout that fires after five failed verifications is per-account, not per-IP. The counter clears on a successful sign-in. Practical consequence: if you keep retrying with wrong codes after the 15-minute window expires, the counter is still at five and the lockout re-engages on the first wrong code. To break the cycle, either wait until you have a known-good code (clock synced authenticator) and sign in cleanly, or have an administrator reset your 2FA from the Users panel to clear both the enrollment and the failure counter at once.
The 15-minute lockout that fires after five failed verifications is per-account, keyed to the user, not to where the request came from. The counter clears on a successful sign-in. Practical consequence: if you keep retrying with wrong codes after the 15-minute window expires, the counter is still at five and the lockout re-engages on the first wrong code. To break the cycle, either wait until you have a known-good code (clock synced authenticator) and sign in cleanly, or have an administrator reset your 2FA from the Users panel to clear both the enrollment and the failure counter at once.
This is distinct from the shared, per-network-address limit described in [Rate limiting](#rate-limiting) above. An administrator reset clears the per-account lockout, but it cannot lift the shared per-address limit; that one only clears when its own 15-minute window elapses.
## Troubleshooting
@@ -231,6 +239,9 @@ The 15-minute lockout that fires after five failed verifications is per-account,
If Sencho runs in a container, the container's clock follows the host. Make sure the host clock is synced with NTP.
</Accordion>
<Accordion title="This code was already used">
Sencho rejects a TOTP code that was already accepted once inside the same 30-second window, to prevent a captured code from being replayed. This is most often triggered by a double-submit on a slow connection or by re-typing a code you already used to sign in seconds earlier. Wait for your authenticator to generate the next code and try again. This rejection counts toward the five-attempt lockout counter, so repeated replays can still trigger the 15-minute lockout described in [Rate limiting](#rate-limiting).
</Accordion>
<Accordion title="Wrong account selected in the authenticator">
Authenticator apps let you store many accounts. The label Sencho uses is the literal issuer `Sencho` plus your username. If you have more than one Sencho instance enrolled in the same authenticator, every entry shows the same issuer; rename them in the app to distinguish (most apps let you edit the label without invalidating the secret).
</Accordion>
@@ -252,6 +263,9 @@ The 15-minute lockout that fires after five failed verifications is per-account,
- **Your own toggle.** **Settings · Account · Two-factor authentication · Require 2FA on SSO sign-in** is on for your account. Flip it off if SSO alone is enough for your threat model.
- **The provider's toggle.** Each SSO provider config has a `Require MFA` switch. When that is on, every SSO-provisioned user must enrol TOTP after their first successful sign-in. Only an administrator can change this on the provider config under **Settings · SSO**.
</Accordion>
<Accordion title="Error reads Too many attempts, please try again in 15 minutes on the plain sign-in form">
This is the shared per-network-address limit from [Rate limiting](#rate-limiting), not the per-account MFA lockout; it can appear on the plain username/password form before 2FA is even reached, and it affects every account signing in from that address. It clears on its own after 15 minutes. An administrator 2FA reset does not lift it, since it is not tied to any one account.
</Accordion>
<Accordion title="Repeatedly locked out after waiting 15 minutes">
The 15-minute window expires on the clock, but the failure counter only resets on a successful sign-in. If you retry with another wrong code after the window expires, the counter is still at five and the lockout re-engages immediately. Have an administrator reset your 2FA from the Users panel to clear both the enrolment and the failure counter, then sign in with your password and re-enrol TOTP from your account settings.
</Accordion>
+68 -29
View File
@@ -1,13 +1,13 @@
---
title: "Vulnerability Scanning"
sidebarTitle: Vulnerability scans
description: "Scan container images and stack compose files for CVEs, secrets, and misconfigurations. Surface severity badges in the Resources Hub, compare scans over time, and gate deploys on policy violations."
description: "Scan container images and stack compose files for CVEs, secrets, and misconfigurations. Surface severity badges in the Resources Hub, prioritize with known-exploited and EPSS evidence, compare scans over time, and gate deploys on policy violations."
---
Sencho integrates with [Trivy](https://trivy.dev) to scan container images and Compose files for vulnerabilities (CVEs), hardcoded secrets, and misconfigurations. Findings surface as severity badges in the Resources Hub and as drillable reports in the scan drawer. The dedicated [Security page](/features/security) is the command center for risk review: overview, image findings, Compose risks, secrets, scan history, suppressions, and scanner setup all live there. Manual scanning, secret and misconfig detection, scan history, comparison, scheduled fleet scans, CVE suppressions, policy enforcement, SARIF export, single-scan SBOM export, and managed Trivy auto-update are all available on every tier.
Sencho integrates with [Trivy](https://trivy.dev) to scan container images and Compose files for vulnerabilities (CVEs), hardcoded secrets, and misconfigurations. Findings surface as severity badges in the Resources Hub and as drillable reports in the scan drawer, enriched with known-exploited (KEV) and EPSS exploit-probability evidence so severity alone is not the only signal. The dedicated [Security page](/features/security) is the command center for risk review: an action-posture overview, image findings, Compose risks, secrets, scan history, suppressions, and scanner setup all live there. Manual scanning, secret and misconfig detection, scan history, comparison, scheduled fleet scans, CVE suppressions with OpenVEX triage, exploit intelligence, risk-based deploy policies, SARIF export, single-scan SBOM export, and managed Trivy auto-update are all available on every tier.
<Frame>
<img src="/images/vulnerability-scanning/resources-badges.png" alt="Resources Hub Images table with severity badges (CRITICAL, HIGH, MEDIUM) on managed image rows alongside the Scan history button" />
<img src="/images/vulnerability-scanning/resources-badges.png" alt="Resources Hub Images table with severity badges (CRITICAL, HIGH, MEDIUM, Clean, and Findings) on managed image rows alongside the Scan history button" />
</Frame>
## Prerequisites
@@ -26,13 +26,16 @@ Every vulnerability-scanning capability is available on every tier:
- On-demand image scanning and full scans (vulnerabilities + secrets)
- Compose file misconfiguration scanning
- Severity badges in the Resources Hub and a scan results drawer with grouped tabs
- Exploit intelligence: CISA known-exploited (KEV) and FIRST EPSS evidence tags on every finding
- Post-deploy automated scanning and a pre-deploy scan advisory on manual deploys
- Scan history sheet and scan comparison
- CVE suppressions and misconfig acknowledgements
- CVE suppressions with OpenVEX triage decisions, OpenVEX export, and misconfig acknowledgements
- Scheduled fleet scans (all images on a node)
- Scan policies with `block_on_deploy` enforcement, including suppression-aware deploy blocking
- Risk-based scan policies (severity threshold, known-exploited, fixable Critical/High) with `block_on_deploy` enforcement, including suppression-aware deploy blocking
- SBOM generation (SPDX, CycloneDX) and SARIF export for code-scanning integration
The [Security page](/features/security) reads these results into an **action posture** (Action needed, Monitoring, Secure, Unknown) that prioritizes fixable and known-exploited findings over raw severity counts. This page covers the scanning mechanics behind that posture: how scans run, what a finding's evidence tags mean, and how policies and suppressions are configured.
## On-demand scanning
Open the **Resources** tab and switch to the **Images** panel. When Trivy is available, every image row shows a shield icon alongside the inspect and delete actions. Click it and the menu offers two scan modes:
@@ -49,11 +52,11 @@ The **Security** page → **Overview** tab has a **Scan this node** button for a
- **Image vulnerabilities** and **Image secrets** scan every image on the node.
- **Compose misconfigurations** scan every stack's compose file.
All three are selected by default; pick any combination, then **Start scan**. Progress streams live in a modal as each image and stack is processed, and the Overview refreshes when the run finishes. The scan stays bound to the node you started it on, so switching the active node mid-scan does not redirect it. Results feed the same severity badges, history, and charts as a single-image scan, and image results reuse the 24-hour digest cache.
All three are selected by default; pick any combination, then **Start scan**. Progress streams live in the [deploy progress](/features/deploy-progress) modal as each image and stack is processed, and the Overview refreshes when the run finishes. The scan stays bound to the node you started it on, so switching the active node mid-scan does not redirect it. Results feed the same severity badges, history, and charts as a single-image scan, and image results reuse the 24-hour digest cache. A partial failure across images or stacks surfaces as a warning toast rather than reading as a clean run.
### Reading severity badges
Hovering a badge reveals the breakdown by severity and the scan timestamp. The badge color reflects the highest severity found:
The badge color reflects the highest severity found:
| Badge | Meaning |
|-------|---------|
@@ -61,16 +64,34 @@ Hovering a badge reveals the breakdown by severity and the scan timestamp. The b
| **HIGH** (orange) | At least one high-severity vulnerability |
| **MEDIUM** (blue) | Only medium and lower vulnerabilities |
| **LOW** (muted) | Only low-severity vulnerabilities |
| **Clean** (green) | Scan completed with zero findings |
| **Clean** (green) | Scan completed with zero findings of any kind |
| **Findings** (muted) | No CVEs, but the scan found secrets or Compose misconfigurations |
Hovering a badge shows when the scan ran and, for a vulnerability badge, a per-severity breakdown (`3C 5H 2M 1L`); for a Findings badge it shows the secret and misconfig counts instead.
Scan results are cached by image digest (see [Tags vs digests](#tags-vs-digests)). If the same digest is scanned again within 24 hours, Sencho returns the cached result instantly instead of re-running Trivy. To force a fresh scan, open the drawer and click **Re-scan**.
### Evidence tags
Severity is one signal among several, not the only one. Every vulnerability finding in the scan drawer and comparison sheet carries independently-verifiable evidence tags so you can tell scary from exploitable at a glance:
| Tag | Meaning |
|-----|---------|
| **KEV** | The CVE is on the CISA Known Exploited Vulnerabilities catalog: it has a confirmed history of active exploitation. |
| **EPSS N%** | The FIRST EPSS score: the modeled probability this CVE will be exploited in the next 30 days. Tinted amber at 10% or higher. |
| **CVSS N** | The CVSS base score, always shown for context but never used as the sole basis for a block condition. |
| **Won't fix** | The vendor has marked this finding `will_not_fix` or `end_of_life`; no upstream patch is coming. |
A scan's header also shows a **Published service** tag when the image belongs to a stack with a port bound to a non-loopback address or attached to host networking, so you can weigh exposure alongside severity. This reflects the Compose configuration, not live container state, and it is an informational signal only, not a policy gate input.
These tags depend on **exploit intelligence** being enabled: a daily background fetch of the CISA KEV catalog and FIRST EPSS scores. Toggle it on the **Security** page → **Scanner setup** tab; it degrades gracefully when offline (existing tags keep showing, new CVEs simply lack them) and can be turned off entirely for air-gapped or firewalled hosts.
## The scan results drawer
The drawer opens as a right-side sheet with the breadcrumb `Security Scans <image-ref>`. The header surfaces the primary scan actions, the summary header reports severity counts and fixable findings, and the body groups findings across three tabs: **Vulnerabilities**, **Secrets**, and **Misconfigs**.
<Frame>
<img src="/images/vulnerability-scanning/scan-details-sheet.png" alt="Vulnerability scan drawer for lscr.io/linuxserver/swag:latest showing the Re-scan, Compare, CSV, and SARIF header actions, the severity summary (2 CRITICAL, 51 HIGH, 65 MEDIUM, 1 LOW), an SBOM download button, the severity filter pills, and a paged CVE table tinted by severity" />
<img src="/images/vulnerability-scanning/scan-details-sheet.png" alt="Vulnerability scan drawer for fyb3roptik/threadfin:latest showing the Re-scan, Compare, CSV, and SARIF header actions, the severity summary (1 CRITICAL, 23 HIGH, 554 MEDIUM, 134 LOW), an SBOM download button, the severity filter pills, and a paged CVE table with EPSS and CVSS evidence tags under each package" />
</Frame>
### Header actions
@@ -87,11 +108,11 @@ The summary header below the actions reports the per-severity counts, the total,
Severity filter pills narrow the table, and the paginated list shows every CVE found with these columns:
- **CVE ID**: links to [cve.org](https://www.cve.org) for `CVE-…` identifiers and to the [GitHub Advisory Database](https://github.com/advisories) for `GHSA-…` identifiers.
- **Package**: the affected package name and installed version.
- **Package**: the affected package name and installed version, with the [evidence tags](#evidence-tags) (KEV, EPSS, CVSS, Won't fix) underneath when applicable.
- **Severity**: badge matching the row tint.
- **Fixed**: the version that contains the fix, with a green checkmark when an upgrade is available.
Critical and high rows carry a left accent rail so the entries that need attention catch the eye even before you read the severity column. If the scan was evaluated against a [scan policy](#scan-policies) and the highest severity meets or exceeds the policy threshold, a destructive **Policy violation** banner appears at the top of the drawer naming the policy and the threshold it crossed.
Critical and high rows carry a left accent rail so the entries that need attention catch the eye even before you read the severity column. Every finding loads (the table paginates, but nothing is capped), so severity filtering, inspection, and suppression reach the full result set even on scans with thousands of CVEs. If the scan was evaluated against a [scan policy](#scan-policies) and any of its block conditions match, a destructive **Policy violation** banner appears at the top of the drawer naming the policy and the reasons it fired (severity threshold, known-exploited, and/or fixable Critical/High).
### Secrets tab
@@ -115,10 +136,10 @@ The deploy itself is never blocked by scanning. Scans run asynchronously and sur
### Opting out per deployment
The App Store deploy sheet has a **Security** section with a **Scan images for vulnerabilities after deploy** checkbox, enabled by default. Uncheck it to skip the post-deploy scan for that single deployment. This does not disable scan policies; it simply opts that deployment out of post-deploy scanning.
The App Store deploy sheet's **Advanced** tab has a **Security** section with a **Scan images for vulnerabilities after deploy** checkbox, enabled by default. Uncheck it to skip the post-deploy scan for that single deployment. This does not disable scan policies; it simply opts that deployment out of post-deploy scanning.
<Frame>
<img src="/images/vulnerability-scanning/app-store-toggle.png" alt="App Store deploy sheet Security section with the auto-scan checkbox enabled" />
<img src="/images/vulnerability-scanning/app-store-toggle.png" alt="App Store deploy sheet Advanced tab scrolled to the Security section, showing the checked 'Scan images for vulnerabilities after deploy' checkbox" />
</Frame>
## Pre-deploy scan advisory
@@ -152,12 +173,12 @@ Failures are usually transient (registry timeouts, missing credentials) and neve
## Scan policies
Policies define severity thresholds that govern whether a stack can deploy. A policy with **Block on deploy** enabled runs a pre-flight scan on every image in the stack before `docker compose up` executes; if any image meets or exceeds the threshold, the deploy is rejected with a dialog listing the offending images. Policies with **Block on deploy** disabled still evaluate every post-deploy and scheduled scan and dispatch warning alerts when the threshold is exceeded.
Policies define **block conditions** that govern whether a stack can deploy: a policy flags an image when any of its enabled conditions match. A policy with **Block on deploy** enabled runs a pre-flight scan on every image in the stack before `docker compose up` executes; if any image matches, the deploy is rejected with a dialog listing the offending images and the reasons they matched. Policies with **Block on deploy** disabled still evaluate every post-deploy and scheduled scan and dispatch warning alerts when a condition matches.
See [Deploy Enforcement](/features/deploy-enforcement) for the full pre-flight flow, admin bypass path, and audit-log behavior.
<Frame>
<img src="/images/vulnerability-scanning/security-settings.png" alt="The Security page Policies tab showing the Deploy enforcement policies section with an Add policy button and the No scan policies configured empty state" />
<img src="/images/vulnerability-scanning/add-policy.png" alt="New policy dialog with a Name field, an optional glob-style Stack pattern field, Block conditions toggles for Severity threshold (off), Known-exploited KEV (on), and Fixable Critical/High (on), a Block on deploy toggle, and an Enabled toggle" />
</Frame>
### Creating a policy
@@ -166,12 +187,14 @@ Go to the **Security** page → **Policies** tab and click **Add policy**.
| Field | Description |
|-------|-------------|
| **Name** | A descriptive label (e.g. `prod-critical-block`). |
| **Name** | A descriptive label (e.g. `prod-high-gate`). |
| **Stack pattern** | Optional glob against stack names (e.g. `prod-*`). Leave empty to match every stack. |
| **Max severity** | The threshold. If a scan finds any vulnerability at or above this severity, the policy fires. |
| **Block on deploy** | When enabled, deploys are rejected before `docker compose up` runs if any image violates the threshold. When disabled, the policy still evaluates post-deploy and scheduled scans and dispatches warning alerts on violations. |
| **Block conditions** | At least one must be enabled to block on deploy: **Severity threshold** flags an image whose highest finding meets or exceeds a chosen severity; **Known-exploited (KEV)** flags an image carrying a CVE on the CISA known-exploited list; **Fixable Critical/High** flags an image with a Critical or High finding that has a fix available. CVSS is always captured for context but is never, on its own, a block condition. |
| **Block on deploy** | When enabled, deploys are rejected before `docker compose up` runs if any image matches an active condition. When disabled, the policy still evaluates post-deploy and scheduled scans and dispatches warning alerts on matches. |
| **Enabled** | Disabled policies are skipped during evaluation. |
New policies default to a **risk-first** configuration: Known-exploited and Fixable Critical/High are on, and the raw Severity threshold is off. A blocking policy with every condition off is rejected, since it would silently block nothing; enable at least one condition before turning on **Block on deploy**.
### Policy scoping
When multiple policies match a deploy, Sencho picks the most specific one:
@@ -188,11 +211,11 @@ Only one policy is evaluated per deploy. Use a single tight pattern rather than
### Example policies
**Block criticals and highs in production.** A tight gate that keeps known vulnerable base images out of your production fleet.
**Block known-exploited and fixable findings in production.** The default risk-first gate: it blocks the CVEs most likely to matter (actively exploited, or patchable right now) without hard-blocking on every Critical CVSS score, many of which have no working exploit.
- **Name:** `prod-high-gate`
- **Name:** `prod-risk-gate`
- **Stack pattern:** `prod-*`
- **Max severity:** `HIGH`
- **Block conditions:** Known-exploited (KEV) on, Fixable Critical/High on, Severity threshold off
- **Block on deploy:** On
- **Enabled:** On
@@ -200,18 +223,22 @@ Only one policy is evaluated per deploy. Use a single tight pattern rather than
- **Name:** `staging-critical-alert`
- **Stack pattern:** `staging-*`
- **Max severity:** `CRITICAL`
- **Block conditions:** Severity threshold on at `CRITICAL`
- **Block on deploy:** Off
- **Enabled:** On
### Honoring suppressions in deploy blocks
By default a block-on-deploy policy evaluates the **raw** scan result, so a CVE you have accepted in [CVE Suppressions](/features/cve-suppressions) still counts toward the block. That keeps the gate strict: suppressions silence alerts and dim findings in reports, but on their own they do not open the deploy path.
By default a block-on-deploy policy evaluates the **raw** (non-suppressed) scan result, so a CVE you have accepted in [CVE Suppressions](/features/cve-suppressions) still counts toward the block. That keeps the gate strict: suppressions silence alerts and dim findings in reports, but on their own they do not open the deploy path.
To have an accepted CVE stop counting toward the gate, turn on **Honor suppressions in deploy blocks** on the **Security** page → **Policies** tab. With it on, the gate re-derives each image's severity from the findings that remain after suppressions are applied, so an image whose only blocking findings are all suppressed deploys without a manual bypass. When a deploy proceeds for this reason, Sencho records it in the audit log so the suppression-driven pass stays traceable.
To have an accepted CVE stop counting toward the gate, turn on **Honor suppressions in deploy blocks** on the **Security** page → **Policies** tab. With it on, the gate re-derives each image's block conditions from the findings that remain after suppressions are applied, so an image whose only blocking findings are all suppressed deploys without a manual bypass. When a deploy proceeds for this reason, Sencho records it in the audit log so the suppression-driven pass stays traceable.
The toggle governs the Sencho instance that runs the deploy and is off by default. Enable it on each instance whose deploys should honor suppressions.
<Note>
A finding whose exploitability can't be confirmed fails **closed** on the known-exploited and fixable conditions: if the scan detail can't be read in full (a truncated cache hit or a database error), the gate treats KEV and fixable conditions as still active rather than silently skipping them. The severity condition still gates from the scan's stored aggregate either way.
</Note>
### Creating a policy via the API
Policy CRUD endpoints are documented in the [Security API reference](/api-reference/security). A typical create call from CI looks like this:
@@ -221,20 +248,29 @@ curl -X POST https://your-sencho-instance:1852/api/security/policies \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "prod-high-gate",
"name": "prod-risk-gate",
"stack_pattern": "prod-*",
"max_severity": "HIGH",
"block_on_severity": 0,
"block_on_kev": 1,
"block_on_fixable": 1,
"block_on_deploy": 1,
"enabled": 1
}'
```
`max_severity` is only enforced when `block_on_severity` is `1`; it still must be one of `CRITICAL`, `HIGH`, `MEDIUM`, or `LOW`.
## Secret detection
Full scans ask Trivy to walk the image filesystem for patterns that look like hardcoded credentials, API tokens, cloud access keys, or private keys. Detection rules cover the common providers (AWS, GCP, GitHub, Slack, Stripe) plus generic high-entropy strings.
Trigger a full scan from the shield-icon menu on any image row with **Full scan (vulnerabilities + secrets)**. Findings appear on the **Secrets** tab of the drawer:
<Frame>
<img src="/images/vulnerability-scanning/secrets-tab.png" alt="Secrets tab of a scan drawer listing five findings: three CRITICAL Google GCP Service-account matches and two MEDIUM JWT token matches, each with a redacted excerpt and a target file path" />
</Frame>
| Column | Description |
|--------|-------------|
| **Severity** | Trivy-assigned severity for the rule that matched. |
@@ -257,7 +293,7 @@ Beyond package CVEs, Sencho can run `trivy config` against a stack's Compose fil
From any stack page, open the **More actions** overflow menu next to the Update button and select **Scan config**. Sencho runs the scanner against the stack's working directory and opens the scan drawer on the **Misconfigs** tab.
<Frame>
<img src="/images/vulnerability-scanning/scan-config-button.png" alt="Plex stack page with the More actions overflow menu open, showing the Scan config item with a shield icon and the Delete item below" />
<img src="/images/vulnerability-scanning/scan-config-button.png" alt="Plex stack page with the More actions overflow menu open, showing Rollback, Scan config with a shield icon, Mute, and Delete" />
</Frame>
The Misconfigs tab columns:
@@ -371,7 +407,7 @@ Every scan Sencho runs is stored with its full vulnerability detail. History ret
Open the **Security** page and select the **History** tab to browse completed scans. The table prefers digest identity when available (short digest with the image reference as a subtitle), lets you search by image reference or digest, and lets you tick two scans to compare. The **Scan history** button at the top of the Resources Hub is a shortcut to the same place.
<Frame>
<img src="/images/vulnerability-scanning/scan-history-sheet.png" alt="Security History tab listing completed scans with search, compare selection, and pagination" />
<img src="/images/vulnerability-scanning/scan-history-sheet.png" alt="Security History tab listing completed scans with digest as the primary label, image reference as the subtitle, Trigger, Severity, Findings, and Fixable columns, a search box, and a Compare (0/2) button" />
</Frame>
## Comparing scans
@@ -395,7 +431,7 @@ Cross-image comparisons (picking scans from two different digests, or different
Up to 1000 findings per scan are loaded for comparison. When a scan exceeds this limit, the sheet shows a truncation banner indicating the comparison may be incomplete.
<Frame>
<img src="/images/vulnerability-scanning/scan-compare-sheet.png" alt="Compare scans sheet diffing two profilarr scans, with baseline and current row, a truncation banner, a delta ribbon (HIGH -79, MEDIUM +79), Added/Removed/Unchanged filter pills, and a diff table tinted by severity" />
<img src="/images/vulnerability-scanning/scan-compare-sheet.png" alt="Compare scans sheet diffing two plex scans, with baseline and current rows, a cross-identity warning banner, a delta ribbon (HIGH +5, MEDIUM +32, LOW -16), Added/Removed/Shared filter pills, and a diff table of added CVEs" />
</Frame>
## How it works
@@ -434,7 +470,10 @@ Up to 1000 findings per scan are loaded for comparison. When a scan exceeds this
When a post-deploy scan fails for a specific image (for example because Trivy could not resolve a private registry pull), Sencho dispatches a warning-level alert through your configured notification channels. The deploy itself is never blocked by a scan failure.
</Accordion>
<Accordion title="A deploy was blocked by a policy I did not expect">
The block dialog names the policy that fired and lists every image that violated the threshold. Open the **Security** page → **Policies** tab and review the matching policy: check the stack pattern glob and the max severity. If the policy should not apply, tighten the pattern (for example `staging-*` instead of `*`) or turn **Block on deploy** off to keep the evaluation in alert-only mode. Admins can bypass a single deploy with the **Deploy anyway** button; every bypass is recorded in the [Audit Log](/features/audit-log) with the actor, policy, and violation list.
The block dialog names the policy that fired and lists every image that matched, along with which conditions matched (severity threshold, known-exploited, and/or fixable Critical/High). Open the **Security** page → **Policies** tab and review the matching policy: check the stack pattern glob and which block conditions are enabled. Remember that new policies default to blocking on known-exploited and fixable findings even when the severity threshold is off, so a CVSS-low finding can still trigger a block if it is on the KEV list. If the policy should not apply, tighten the pattern (for example `staging-*` instead of `*`), disable the condition that is too aggressive, or turn **Block on deploy** off to keep the evaluation in alert-only mode. Admins can bypass a single deploy with the **Deploy anyway** button; every bypass is recorded in the [Audit Log](/features/audit-log) with the actor, policy, and violation list.
</Accordion>
<Accordion title="Known-exploited and EPSS tags are missing from findings">
These evidence tags depend on **exploit intelligence**, a daily background fetch of the CISA KEV catalog and FIRST EPSS scores. Confirm the toggle is on under the **Security** page → **Scanner setup** tab. If it is on but tags are still missing, the host may be offline or firewalled: the feature degrades gracefully rather than failing the scan, so existing tags keep showing but new CVEs will not get fresh KEV/EPSS data until connectivity is restored. Air-gapped hosts can leave the toggle off intentionally; severity, CVSS, and fix-availability data are unaffected either way.
</Accordion>
<Accordion title="Trivy is not installed and a deploy with a block policy went through">
Sencho fails open when Trivy is not installed on the target node, so operators are never locked out by tooling state. A warning alert is dispatched through your configured notification channels with the message `Pre-deploy scan for "<stack>" skipped: Trivy not installed on this node`. Install Trivy from the **Security** page → **Scanner setup** tab to enforce the policy; see [Installing Trivy](/operations/trivy-setup) for options.
+13 -3
View File
@@ -34,7 +34,7 @@ Open **Settings → Automation → Webhooks** and click **Create webhook**. The
After you click **Create**, the new webhook's secret appears once in a green callout titled **Webhook created. Copy your secret now.** Copy it before dismissing the callout; once you navigate away, it cannot be retrieved. The configured-webhooks list always shows the secret in masked form (`********` plus the last four characters).
<Frame>
<img src="/images/webhooks/webhooks-secret-reveal.png" alt="Green success callout after creating a webhook, showing the full secret in a monospace block, a Copy button, and a Dismiss button. Below the callout, the configured-webhooks list shows the new webhook card with its trigger URL and masked secret." />
<img src="/images/webhooks/webhooks-secret-reveal.png" alt="Green success callout after creating a webhook, showing the full secret in a monospace block and a Copy button next to a Dismiss button. Below the callout, the configured-webhooks list shows the new webhook card with its trigger URL, masked secret, and Recent executions expanded to show one successful deploy entry with its timestamp and duration." />
</Frame>
### Available actions
@@ -52,10 +52,10 @@ After you click **Create**, the new webhook's secret appears once in a green cal
Each configured webhook renders as a card showing the webhook name, an **action** badge, a **stack** badge, a **node** badge, an enable toggle, and a delete button. Below the header are a labeled **Trigger URL** row with a copy button, a labeled **Secret** row showing the masked secret, and a **Recent executions** disclosure that expands inline.
The masthead above the section reports total **Webhooks** and how many are **Enabled**.
The masthead carries `SCOPE` (`global`), `WEBHOOKS` (total), and `ENABLED` (count).
<Frame>
<img src="/images/webhooks/webhooks-settings.png" alt="Configured webhooks section with two webhook cards. The first card has Recent executions expanded, showing several deploy entries with timestamps and durations and one restart entry that failed with an error message. The second card is collapsed. The masthead at the top shows WEBHOOKS 2 and ENABLED 2." />
<img src="/images/webhooks/webhooks-settings.png" alt="Configured webhooks section with two webhook cards, each showing its name, action badge, stack badge, node badge, an On toggle, and a delete icon. The first card's Recent executions is collapsed; the second card's is expanded, showing one successful deploy entry with its timestamp and duration. The masthead at the top shows SCOPE global, WEBHOOKS 2, and ENABLED 2." />
</Frame>
From a webhook card you can:
@@ -102,6 +102,12 @@ By default, the webhook executes the action configured at creation time. You can
Valid override values are: `deploy`, `restart`, `stop`, `start`, `pull`, `git-pull`. An unknown value is rejected with `400 Bad Request` before execution is queued.
### Concurrency and the per-stack lock
Deploy, Restart, Stop, Start, and Pull & Update share the same per-stack lock as manual actions started from the UI. If the stack already has one of those operations in flight when the webhook fires, Sencho still returns `202 Accepted`, but the action itself is skipped rather than queued behind the running one. The skip shows up in **Recent executions** as a failed entry with a message like `Skipped "stack-name": another operation (deploy) is already in progress.` Retry the trigger once the in-flight operation finishes.
Git source sync is the exception: it waits for an in-progress pull or apply on the same stack to finish rather than skipping (see [Local edits vs Git](/features/git-sources#local-edits-vs-git)).
### Responses
| Status | Body | Meaning |
@@ -181,6 +187,10 @@ Sencho retains the last 100 executions per webhook and surfaces the 20 most rece
A 202 means the action was accepted, not that it produced a visible change. **Pull & Update** is a no-op if the registry has no newer image for any service in the stack; **Start** is a no-op on an already-running stack. Check the stack's **Activity** sheet to confirm what actually ran, and the webhook's **Recent executions** for the action's status and duration.
</Accordion>
<Accordion title="Recent executions shows 'Skipped ... another operation is already in progress'.">
The webhook's action shares the stack's per-stack lock with manual actions from the UI. The webhook fired while a manual deploy, update, restart, stop, start, rollback, or backup on the same stack was still running, so Sencho skipped the webhook's action rather than queuing it behind the running one. Retry the trigger once the in-flight operation finishes. Git source sync does not hit this path; it waits for the in-progress operation instead of skipping.
</Accordion>
<Accordion title="My trigger returns 429 'Too many webhook triggers'.">
The trigger endpoint is rate limited to 500 requests per minute per source IP. CI/CD runners behind shared NAT or a datacenter egress pool all share that budget. Either space the calls out, send them from a less crowded egress, or split the work across multiple webhooks if a single workflow fires hundreds of triggers in close succession.
</Accordion>
+8 -2
View File
@@ -14,7 +14,7 @@ Operational settings (host alerts, data retention, image-update automation, mesh
| `COMPOSE_DIR` | Absolute path to the directory that contains your Compose stacks. Every subdirectory inside becomes a stack in Sencho. Falls back to `/app/compose` if unset, but per the [1:1 path rule](#compose-directory-the-11-path-rule) you should always set this explicitly to match the host path you mount. |
<Note>
**JWT_SECRET is generated automatically.** Sencho creates a secure random signing key during initial setup and stores it in its database. You do not need to provide one.
**The JWT signing secret is generated automatically.** Sencho creates a secure random signing key during initial setup and stores it in its database. There is no environment variable for it: you cannot set or override it, and you do not need to.
</Note>
### How Sencho organizes your compose directory
@@ -35,6 +35,7 @@ When you point `COMPOSE_DIR` at a directory, Sencho expects each stack to live i
| `SENCHO_USER` | *(unset)* | When set to a username present inside the container (`sencho` is pre-created for this purpose), the entrypoint drops privileges to that user at startup instead of running as `root`. See [Running as a non-root user](#running-as-a-non-root-user) below. |
| `API_RATE_LIMIT` | `200` | Global API requests per minute per user session. Applies in production only; development uses a fixed higher cap. Authenticated requests are keyed by user ID, unauthenticated by IP. Internal node-to-node traffic bypasses this limit. |
| `API_POLLING_RATE_LIMIT` | `300` | Rate limit for dashboard polling endpoints, in requests per minute. Applies in production only; development uses a fixed higher cap. Raise it for environments with many concurrent browser sessions behind shared NAT. |
| `SENCHO_UPLOAD_DIR` | OS temp dir (`sencho-uploads` subfolder) | Staging directory for file uploads in the Stack file editor. Uploads spool to disk here, outside `COMPOSE_DIR`, before being written to their final location. Set this if your OS temp partition is too small for the uploads you expect. |
## Advanced environment variables
@@ -43,10 +44,14 @@ These tune optional subsystems. Most deployments never set them; the defaults ar
| Variable | Default | Description |
|----------|---------|-------------|
| `TRIVY_BIN` | *(unset)* | Path to a host-installed [Trivy](/operations/trivy-setup) binary for image vulnerability scanning. Sencho prefers a managed install under `DATA_DIR/bin/trivy`, then this path, then `trivy` on `PATH`. |
| `TRIVY_CACHE_DIR` | `DATA_DIR/trivy-cache` | Directory where Trivy caches its vulnerability database. Move it onto a different volume if `DATA_DIR` is space-constrained. |
| `SENCHO_MESH_SUBNET` | *(auto)* | CIDR for this node's `sencho_mesh` network. When unset, Sencho picks the first free `/24` from its candidate list or adopts an existing mesh subnet. Set one only to avoid an overlap with another network on the host. See [Sencho Mesh](/features/sencho-mesh). |
| `SENCHO_MESH_RECONCILE_INTERVAL_MS` | `60000` | How often the central instance re-checks proxy-mode mesh tunnels to detect a peer that rebooted. Lower it for faster peer-reboot detection at the cost of more frequent checks. See [Sencho Mesh](/features/sencho-mesh). |
| `SENCHO_MESH_PROXY_TUNNEL_IDLE_MS` | `0` | Idle timeout before a proxy-mode mesh tunnel tears down and reopens on demand. `0` keeps the tunnel open for the life of the connection. See [Sencho Mesh](/features/sencho-mesh). |
| `GITSOURCE_MAX_CLONE_BYTES` | `104857600` | Maximum bytes a single [Git Source](/features/git-sources) clone may download before it is aborted (100 MB). A shallow Compose clone is tiny; raise it only if you track Compose files in a legitimately large repository. |
| `SENCHO_PUBLIC_URL` | *(request host)* | Set on the primary instance. Its externally reachable `http(s)://` URL, no trailing slash, baked into pilot enrollment so remote agents dial the public hostname rather than the address the admin used at setup. |
| `SENCHO_COMPOSE_STALL_TIMEOUT_MS` | `600000` | Idle-output backstop for deploy and update Compose steps (pull and recreate). If a step produces no output for this long while still running, Sencho stops it so a hung image pull surfaces a clear failure and the in-app recovery actions instead of spinning. Raise it on slow links or for heavy local image builds. |
| `SENCHO_COMPOSE_COMMAND_TIMEOUT_MS` | `1800000` | Hard timeout for a single Compose command (pull, up, down) during deploy and update, in milliseconds (30 minutes). Sencho kills the command and reports failure if it runs longer than this, regardless of whether it is still producing output. Raise it only for very large images or slow storage. |
| `SENCHO_COMPOSE_STALL_TIMEOUT_MS` | `600000` | Idle-output backstop for deploy and update Compose steps (pull and recreate), separate from the hard timeout above. If a step produces no output for this long while still running, Sencho stops it so a hung image pull surfaces a clear failure and the in-app recovery actions instead of spinning. Raise it on slow links or for heavy local image builds. |
| `SENCHO_ZFS_ARCSTATS_PATH` | *(auto)* | Path **inside the container** to the OpenZFS ARC kstat file, for [ZFS ARC-aware host memory](#zfs-arc-aware-host-memory). Sencho checks this path first, then `/host/proc/spl/kstat/zfs/arcstats`, then `/proc/spl/kstat/zfs/arcstats`. Set it only when your ARC stats live at a non-standard path. |
Running a remote host as a pilot agent uses four more variables (`SENCHO_MODE`, `SENCHO_PRIMARY_URL`, `SENCHO_ENROLL_TOKEN`, and `SENCHO_PILOT_CA_FILE`), set only on the remote agent container. Sencho bakes them into the enrollment Compose file it generates, so you rarely write them by hand. See [Pilot Agent](/features/pilot-agent) for the full enrollment walkthrough.
@@ -117,6 +122,7 @@ If you use SSO, configure your identity providers via environment variables:
| `SSO_OIDC_GOOGLE_ENABLED` | Enable Google SSO |
| `SSO_OIDC_GITHUB_ENABLED` | Enable GitHub SSO |
| `SSO_OIDC_OKTA_ENABLED` | Enable Okta SSO |
| `SSO_OIDC_CUSTOM_ENABLED` | Enable a custom OIDC provider (Keycloak, Authentik, Authelia, Zitadel, and others) |
| `SSO_CALLBACK_URL` | External base URL for OAuth callbacks (required behind reverse proxy) |
For end-to-end provider setup walkthroughs, see the [SSO Quickstart →](/getting-started/sso-quickstart). For the full feature reference, see [SSO Authentication →](/features/sso).
+3 -3
View File
@@ -49,10 +49,10 @@ Stacks can be pinned and grouped by label. Each row carries a compact status mar
Opening a stack gives you the day-to-day workspace:
- The identity header shows the node, stack name, runtime state, and main action buttons.
- Running stacks expose **Restart**, **Stop**, and **Update**; stopped stacks expose **Start** and **Update**. The overflow menu holds less frequent actions such as rollback, config scan, and delete.
- Running stacks expose **Restart**, **Stop**, **Take down**, and **Update**; stopped stacks expose **Start** and **Update**. The overflow menu holds less frequent actions such as rollback, config scan, and delete.
- Container rows show health, uptime, published ports, live CPU and memory, network activity, logs, and service actions.
- The logs panel can run in **Structured** mode or **Raw terminal** mode.
- The right panel provides tabs for **Anatomy**, **Activity**, **Dossier**, **Drift**, **Environment**, **Networking**, **Doctor**, and **Storage**, with **Files** and **Edit** controls for browsing stack files and editing compose or env content.
- The right panel provides tabs for **Anatomy**, **Activity**, **Dossier**, **Drift**, **Environment**, **Compose Labels**, **Networking**, **Doctor**, and **Storage**, with **Files** and **Edit** controls for browsing stack files and editing compose or env content.
## Fleet operations
@@ -64,7 +64,7 @@ The **Fleet** view is the multi-node command center. The masthead summarizes onl
The Fleet toolbar includes **Check Updates**, **Refresh**, and **Add node** for admins. The **Overview** tab supports search, sort, status filters, label filters, and a Grid or Topology view. Node cards show online state, resource use, container counts, version state, update actions, and direct drill-down into stacks on that node.
Beyond **Overview**, Fleet provides tabs for **Snapshots**, node **Status**, a dependency **Map**, blueprint **Deployments**, **Federation**, and fleet **Actions**. Federation placement (cordon and pin) is available on every tier. Routing and Secrets are limited-availability tabs when enabled on an instance. See [Licensing](/features/licensing) for the full tier breakdown.
Beyond **Overview**, Fleet provides tabs for **Snapshots**, node **Status**, a dependency **Map**, a **Docker Labels** audit, blueprint **Deployments**, **Federation**, and fleet **Actions**. Federation placement (cordon and pin) is available on every tier. Routing and Secrets are limited-availability tabs when enabled on an instance. See [Licensing](/features/licensing) for the full tier breakdown.
## Resources, templates, and logs
+5 -5
View File
@@ -87,10 +87,10 @@ Open `http://localhost:1852` in a browser. On a fresh install you land on the **
Pick a username, choose a password, confirm it, and click **Initialize console**. The username placeholder shows `admin`. The password must be at least eight characters, and the strength indicator labels the password **Weak**, **Fair**, or **Strong** as you type.
Sencho then runs a short **environment preflight**: it confirms the Docker engine and Compose plugin are reachable, the compose directory is writable and mounted at a matching host path, the dashboard is behind TLS, and there is disk headroom. Anything that needs attention shows an inline fix. The checks never block you, so click **Enter Sencho** to continue; you can re-run them anytime from **Settings · Recovery**.
Sencho then runs a short **environment preflight**: it confirms the Docker engine and Compose plugin are reachable, the compose directory is writable, Sencho's own container isn't nested inside that same directory, the host and container paths match, the dashboard is behind TLS, and there is disk headroom. Anything that needs attention shows an inline fix, like the disk space warning below. The checks never block you, so click **Enter Sencho** to continue; you can re-run them anytime from **Settings · Recovery**.
<Frame>
<img src="/images/quickstart/setup-environment.png" alt="Sencho first-boot environment preflight with check rows for Docker engine, Docker Compose, Compose directory, path mapping, TLS, and disk space, each showing a pass or warning status with inline remediation text, a Re-run button, and an Enter Sencho button" />
<img src="/images/quickstart/setup-environment.png" alt="Sencho first-boot environment preflight with check rows for Docker engine, Docker Compose, Compose directory, Sencho compose location, path mapping, TLS, and disk space, each showing a pass or warning status with inline remediation text, a Re-run button, and an Enter Sencho button" />
</Frame>
<Note>
@@ -99,7 +99,7 @@ Sencho then runs a short **environment preflight**: it confirms the Docker engin
## After signing in
You land on **Home**, the default operational view. The health masthead reports **Healthy**, **Degraded**, or **Critical**, names the active node, shows how many nodes are registered, and lists any signals that need attention. The resource gauge strip tracks **CPU**, **Memory**, **Disk**, and **Network** with sparklines and threshold coloring. **Stack health** lists the active node's stacks, sorted by state then load, with uptime, CPU, memory, and a 10-minute CPU sparkline per row.
You land on **Home**, the default operational view. The health masthead reports **Healthy**, **Degraded**, or **Critical**, names the active node, shows how many nodes are registered, and lists any signals that need attention, alongside a running-container count, a live CPU/memory readout, and an alert count. The resource gauge strip tracks **CPU**, **Memory**, **Disk**, and **Network** with sparklines and threshold coloring. **Stack health** lists the active node's stacks, sorted by load by default (click a column header to sort by name, uptime, CPU, or memory instead), with each row's source (**Local** or **Git**), port, uptime, CPU, memory, and a 10-minute CPU sparkline.
<Frame>
<img src="/images/quickstart/dashboard.png" alt="Sencho Home view with the health masthead, resource gauges with sparklines, Stack health table, Configuration Status, Fleet Heartbeat panel, and Recent Alerts" />
@@ -107,9 +107,9 @@ You land on **Home**, the default operational view. The health masthead reports
Below the stack table, **Configuration Status** summarizes notifications, alerts, automation, security, backups, thresholds, and crash detection. The neighboring activity card shows **Fleet Heartbeat** when remote nodes exist, or **Stack Restarts (7d)** on a local-only install. **Recent Alerts** shows the latest notification feed and includes **Clear All Notifications** when there is anything to clear.
On the local node, baseline top navigation includes **Home**, **Resources**, **Networking**, **Security**, and **App Store**. **Fleet** appears when your role can read nodes. **Logs**, **Update**, and **Schedules** appear for admins. **Console** and **Audit** depend on license, role, and whether those surfaces are enabled; hub-only views are hidden when a remote node is active. Desktop presentation (Classic bar, Smart bar, or Compact launcher) is chosen under **Settings → Appearance → Navigation**. The right side of the top bar holds global search, notifications, and the profile menu entries **Settings**, **Documentation**, **Feedback**, **Appearance**, and **Log Out**.
On the local node, baseline top navigation includes **Home**, **Resources**, **Networking**, **Security**, and **App Store**. **Fleet** appears when your role can read nodes. **Logs**, **Update**, and **Schedules** appear for admins. **Console** and **Audit** depend on license, role, and whether those surfaces are enabled; hub-only views are hidden when a remote node is active. Desktop presentation (Classic bar, Smart bar, or Compact launcher) is chosen under **Settings → Appearance → Navigation**. The right side of the top bar holds global search, notifications, and the profile menu entries **Settings**, **Billing** (when a paid license is active), **Documentation**, **Open New Issue**, and **Log Out**.
The left sidebar is the stack workspace. Below the Sencho brand, it starts with the node switcher, then **Create Stack**, a bulk-mode toggle, and **Scan stacks folder** for re-indexing compose projects added outside Sencho. Use **Search stacks...** with the **All**, **Up**, **Down**, and **Updates** chips to narrow the list. On a fresh install with an empty stack list, Sencho scans your mounted compose directory automatically and shows what it found, including compose files that still need to be adopted into their own subfolder.
The left sidebar is the stack workspace. Below the Sencho brand, it starts with the node switcher, then **Create Stack**, a bulk-mode toggle, and **Scan stacks folder** for re-indexing compose projects added outside Sencho. Use **Search stacks...** with the **All**, **Up**, **Down**, and **Updates** chips to narrow the list. On a fresh install with an empty stack list, Sencho scans your mounted compose directory automatically and shows what it found, including compose files that still need to be adopted into their own subfolder. Once stacks carry Docker Compose labels, the list groups them under those labels, with pinned stacks always floating to the top and unlabeled stacks collected at the bottom.
Click **Create Stack** in the sidebar and choose **Empty**, **From Git**, or **From Docker Run**. Use **Adopt existing files** when Sencho finds compose files that are loose at the compose directory root or one folder too deep (any `.yml` / `.yaml`, not only `compose.yaml`). Loose root files are also auto-promoted into stack folders on the next Sencho restart; adopt is the guided path especially for nested placements. Open **App Store** for a template-driven deploy flow. Once a stack exists, select it in the sidebar or in **Stack health** to open the stack workspace with container health, logs, files, compose editing, and stack actions.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 KiB

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Some files were not shown because too many files have changed in this diff Show More