+
## Notification channels
Open **Settings · Notifications · Channels** to configure Discord, Slack, custom webhook, and Apprise channels. Each channel is per-node, so switching the active node via the node picker reloads the panel against that node's stored settings. The masthead carries a `CHANNELS` stat showing how many of the four slots are enabled.
+Above the channel tabs, **Delivery retries** sets how many extra in-process attempts (0 to 3) Sencho makes after a transient delivery failure on that node. The default is `0` (single-shot). Extra attempts wait a fixed one second between tries. Admin role is required to change the value.
+
Each Discord, Slack, and Webhook tab carries an **Enabled** toggle, a **Webhook URL** input (HTTPS only), and **Test** / **Save**. The Apprise tab uses an **Apprise endpoint** instead: keyed `/notify/{key}` shows optional **Tags**; stateless `/notify` shows **Destination URLs**. The kicker on each tab toggles between `enabled` and `off` so you can see at a glance which slots are wired up.
+
## Deploying a template
-The deployment sheet slides in from the right. The header carries a breadcrumb (`App store › `), 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 › `), 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.
-
+
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.
-
+
#### 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.
+
+
@@ -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
+
+
+
+
+- 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.
diff --git a/docs/features/atomic-deployments.mdx b/docs/features/atomic-deployments.mdx
index a3b0bc8c..07954e5e 100644
--- a/docs/features/atomic-deployments.mdx
+++ b/docs/features/atomic-deployments.mdx
@@ -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.
-
+
-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.
+
+
+
+
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.
+
**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.
-
+
-
+
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
+
+
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
+
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.
-
+
- **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.
-
+
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.
+
+
### 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.
-
+
### 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.
@@ -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.
@@ -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 [
/>
-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
@@ -55,7 +55,7 @@ A few things worth knowing:
@@ -113,8 +113,8 @@ See [Compose Doctor](/features/compose-doctor) for the full rule reference and h
+
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. |
-
+
### 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:
-
+
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.
+
+
+
+
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.
+
+
+
+
+
+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.
+
## 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.
-
+
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.
-
+
| 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.
-
+
+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 Enter or Space, 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 Enter or Space, 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.
-
+
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 `
+
The header carries a radio-tower icon, the total node count, and a `
+
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
+
+
+
+
+
+The phone layout condenses the dashboard to what fits a single thumb-scroll: the masthead (state word, status dot, and a compact `
+
| 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.
+
-
+
The dialog shows:
diff --git a/docs/features/deploy-progress.mdx b/docs/features/deploy-progress.mdx
index 70cae529..54c890bd 100644
--- a/docs/features/deploy-progress.mdx
+++ b/docs/features/deploy-progress.mdx
@@ -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 `
+
## 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.
-
+
## Auto-close on success
-When an action completes successfully, the status indicator switches to `Succeeded · closes in
+
## 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
+
| 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.
+
+
+
-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.
+
+
+
+
+
+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
+
+
+
-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
+
+
+
+
## 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
+
## 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.
-
+
## Opening the inventory
diff --git a/docs/features/fleet-actions.mdx b/docs/features/fleet-actions.mdx
index 87b0430b..4cbe6556 100644
--- a/docs/features/fleet-actions.mdx
+++ b/docs/features/fleet-actions.mdx
@@ -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.
-
+
+
+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.
-
+
A few quirks worth knowing:
-- A node that has no stack label by that name appears as `
+
### 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.
-
+
### 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:
+
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: `
+
## 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 `
+
-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.
-
+
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.
-
+
### 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.
-
+
### 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.
+
+
## Troubleshooting
+
+
-
-
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.
-
-
+
+
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.
-
-
+
+
-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.
+
+
+
-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
+
+
+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.
+
+
+
+
+
+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:
+
+
+
+
+
+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
+
## 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).
+
+
+
+
+### 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:
+
+
+
+
+
+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:
+
+
+
+
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.
-
-
-
-
+- **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/
+
## 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 `
+
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 `
+
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.
-
+
-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 `
+
### 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.
+
## 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.
-
+
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.
-
+
| 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.
-
+
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 `
+
| 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.
-
+
-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
+
## Opening the palette
-Three ways to open it:
+Two ways to open it:
- Press Ctrl + K (or Cmd + K on macOS) from anywhere in the app.
-- Click the search icon in the top bar, left of the notification bell.
-- Press Esc 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 Esc 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.
-
+
## 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.
-
+
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.
+
+
+
+
+
+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.
+
+
+
+
+
## Troubleshooting
+
## The post-update health gate
diff --git a/docs/features/licensing.mdx b/docs/features/licensing.mdx
index 7b543c95..5ce45db3 100644
--- a/docs/features/licensing.mdx
+++ b/docs/features/licensing.mdx
@@ -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.
+
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.
-
+
-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:
+
-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.
-
+
## Multi-node license enforcement
diff --git a/docs/features/multi-node.mdx b/docs/features/multi-node.mdx
index 6eaeb43e..90d138a2 100644
--- a/docs/features/multi-node.mdx
+++ b/docs/features/multi-node.mdx
@@ -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.
-
+
## 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.
-
+
-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
-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.
-
+
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:
-
+
| 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.
+
-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.
-
+
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.
-
+
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 |
+
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
+
-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.
-
+
-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.
-
-
-
+
+
## 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.
+
+
- **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
-
+
-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.
+
+
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.
diff --git a/docs/features/scheduled-operations.mdx b/docs/features/scheduled-operations.mdx
index f520f166..79100e88 100644
--- a/docs/features/scheduled-operations.mdx
+++ b/docs/features/scheduled-operations.mdx
@@ -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.
-
+
## 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.
-
+
| 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.
-
+
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.
-
+
### 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).
-
+
### 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.
-
+
### 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 › `
+
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
+
+
+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
+
+
+
+
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.
+
+
+
+
## Compose risks
+
+
+
+
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
+
+
+
+
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.
+
+
+
+
+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
+
+
+
+
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).
+
+
+
+
+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
+
+
+
+
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
+
+
+
+
+
+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
diff --git a/docs/features/sidebar.mdx b/docs/features/sidebar.mdx
index 31000f5e..971f67b5 100644
--- a/docs/features/sidebar.mdx
+++ b/docs/features/sidebar.mdx
@@ -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.
-
+
## 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.
-
+
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.
-
+
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.
@@ -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.
-
+
## Bulk mode
@@ -84,19 +91,19 @@ Click the **Bulk mode** icon next to **Create Stack** (or press B) 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 B) to leave bulk mode.
+Click rows to toggle their selection, or press Ctrl+A (Cmd+A 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 B to leave bulk mode, or press Esc to leave bulk mode and clear the selection in one step.
-
+
## 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).
+
## 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 Cmd in place of Ctrl.
| H | Open the auto-heal sheet |
| U | Check for image updates |
| P | Pin or unpin the stack |
+
+### Bulk mode
+
+These do not require a selected stack.
+
+| Shortcut | Action |
+|----------|--------|
| B | Toggle bulk mode |
+| Ctrl+A | In bulk mode, select every stack matching the current search and filter chip |
+| Esc | In bulk mode, exit and clear the selection |
+
+On macOS, use Cmd in place of Ctrl.
## 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.
+
## 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
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
+
+
## 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`.
-
+
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.
-
+
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.
## 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.
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
-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.
### 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.
**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.
+
+
+
-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.
-
+
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.
-
+
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.
-
+
**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.
-
+
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.
diff --git a/docs/features/two-factor-authentication.mdx b/docs/features/two-factor-authentication.mdx
index 4cb63b69..3ddc5556 100644
--- a/docs/features/two-factor-authentication.mdx
+++ b/docs/features/two-factor-authentication.mdx
@@ -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.
+
+
+
## 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 ›
+
### 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.
-
+
## 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.
-
+
### 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.
+
+
+
| 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.
-
+
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.
-
+
## 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.
-
+
## 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.
+
### 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).
-
+
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.
+
@@ -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.
diff --git a/docs/images/alerts-notifications/notifications-settings.png b/docs/images/alerts-notifications/notifications-settings.png
index 85004f3a..cc44884b 100644
Binary files a/docs/images/alerts-notifications/notifications-settings.png and b/docs/images/alerts-notifications/notifications-settings.png differ
diff --git a/docs/images/api-tokens/api-tokens-create-form.png b/docs/images/api-tokens/api-tokens-create-form.png
index e9519483..871ce446 100644
Binary files a/docs/images/api-tokens/api-tokens-create-form.png and b/docs/images/api-tokens/api-tokens-create-form.png differ
diff --git a/docs/images/api-tokens/api-tokens-empty-state.png b/docs/images/api-tokens/api-tokens-empty-state.png
index 86f4aa33..0ceec9ad 100644
Binary files a/docs/images/api-tokens/api-tokens-empty-state.png and b/docs/images/api-tokens/api-tokens-empty-state.png differ
diff --git a/docs/images/api-tokens/api-tokens-populated.png b/docs/images/api-tokens/api-tokens-populated.png
index 24580096..e1bf1445 100644
Binary files a/docs/images/api-tokens/api-tokens-populated.png and b/docs/images/api-tokens/api-tokens-populated.png differ
diff --git a/docs/images/api-tokens/api-tokens-reveal-banner.png b/docs/images/api-tokens/api-tokens-reveal-banner.png
index b502f4d0..fb9d67cb 100644
Binary files a/docs/images/api-tokens/api-tokens-reveal-banner.png and b/docs/images/api-tokens/api-tokens-reveal-banner.png differ
diff --git a/docs/images/api-tokens/api-tokens-revoke-modal.png b/docs/images/api-tokens/api-tokens-revoke-modal.png
index 8fdbd593..fca447bb 100644
Binary files a/docs/images/api-tokens/api-tokens-revoke-modal.png and b/docs/images/api-tokens/api-tokens-revoke-modal.png differ
diff --git a/docs/images/app-store/app-store-hero.png b/docs/images/app-store/app-store-hero.png
index 07618cc0..76a6cd91 100644
Binary files a/docs/images/app-store/app-store-hero.png and b/docs/images/app-store/app-store-hero.png differ
diff --git a/docs/images/app-store/app-store-mobile.png b/docs/images/app-store/app-store-mobile.png
index 9cd0d33d..d7c56a2f 100644
Binary files a/docs/images/app-store/app-store-mobile.png and b/docs/images/app-store/app-store-mobile.png differ
diff --git a/docs/images/app-store/app-store-port-conflict.png b/docs/images/app-store/app-store-port-conflict.png
index 7c9d554f..5084ce73 100644
Binary files a/docs/images/app-store/app-store-port-conflict.png and b/docs/images/app-store/app-store-port-conflict.png differ
diff --git a/docs/images/app-store/app-store-settings-registry.png b/docs/images/app-store/app-store-settings-registry.png
index eec281cc..2daf49a2 100644
Binary files a/docs/images/app-store/app-store-settings-registry.png and b/docs/images/app-store/app-store-settings-registry.png differ
diff --git a/docs/images/app-store/deploy-sheet-advanced.png b/docs/images/app-store/deploy-sheet-advanced.png
index 46a56e19..43a00251 100644
Binary files a/docs/images/app-store/deploy-sheet-advanced.png and b/docs/images/app-store/deploy-sheet-advanced.png differ
diff --git a/docs/images/app-store/deploy-sheet-essentials.png b/docs/images/app-store/deploy-sheet-essentials.png
index 3c547b2e..e61369f2 100644
Binary files a/docs/images/app-store/deploy-sheet-essentials.png and b/docs/images/app-store/deploy-sheet-essentials.png differ
diff --git a/docs/images/atomic-deployments/rollback-menu.png b/docs/images/atomic-deployments/rollback-menu.png
index 83056a70..a2420a3c 100644
Binary files a/docs/images/atomic-deployments/rollback-menu.png and b/docs/images/atomic-deployments/rollback-menu.png differ
diff --git a/docs/images/atomic-deployments/rollback-readiness.png b/docs/images/atomic-deployments/rollback-readiness.png
new file mode 100644
index 00000000..0bc30461
Binary files /dev/null and b/docs/images/atomic-deployments/rollback-readiness.png differ
diff --git a/docs/images/audit-log/audit-log-expanded.png b/docs/images/audit-log/audit-log-expanded.png
index 6e116c09..874c3166 100644
Binary files a/docs/images/audit-log/audit-log-expanded.png and b/docs/images/audit-log/audit-log-expanded.png differ
diff --git a/docs/images/audit-log/audit-log-overview.png b/docs/images/audit-log/audit-log-overview.png
index d82d9c89..02fd8af8 100644
Binary files a/docs/images/audit-log/audit-log-overview.png and b/docs/images/audit-log/audit-log-overview.png differ
diff --git a/docs/images/audit-log/audit-stream.png b/docs/images/audit-log/audit-stream.png
index 67564d60..3335ffb2 100644
Binary files a/docs/images/audit-log/audit-stream.png and b/docs/images/audit-log/audit-stream.png differ
diff --git a/docs/images/audit-log/data-retention.png b/docs/images/audit-log/data-retention.png
index 0ad7ea26..ce901bbc 100644
Binary files a/docs/images/audit-log/data-retention.png and b/docs/images/audit-log/data-retention.png differ
diff --git a/docs/images/auto-heal-policies/policy-row.png b/docs/images/auto-heal-policies/policy-row.png
index 13edc279..21913f73 100644
Binary files a/docs/images/auto-heal-policies/policy-row.png and b/docs/images/auto-heal-policies/policy-row.png differ
diff --git a/docs/images/auto-heal-policies/recent-activity.png b/docs/images/auto-heal-policies/recent-activity.png
index cc3cb7ea..ddeef916 100644
Binary files a/docs/images/auto-heal-policies/recent-activity.png and b/docs/images/auto-heal-policies/recent-activity.png differ
diff --git a/docs/images/auto-heal-policies/sheet.png b/docs/images/auto-heal-policies/sheet.png
index 84282d32..b05cec5e 100644
Binary files a/docs/images/auto-heal-policies/sheet.png and b/docs/images/auto-heal-policies/sheet.png differ
diff --git a/docs/images/auto-update-policies/create-dialog.png b/docs/images/auto-update-policies/create-dialog.png
deleted file mode 100644
index c48259bf..00000000
Binary files a/docs/images/auto-update-policies/create-dialog.png and /dev/null differ
diff --git a/docs/images/auto-update-policies/overview.png b/docs/images/auto-update-policies/overview.png
deleted file mode 100644
index 086d9f8b..00000000
Binary files a/docs/images/auto-update-policies/overview.png and /dev/null differ
diff --git a/docs/images/auto-update-policies/run-history.png b/docs/images/auto-update-policies/run-history.png
deleted file mode 100644
index d768cc96..00000000
Binary files a/docs/images/auto-update-policies/run-history.png and /dev/null differ
diff --git a/docs/images/auto-update/readiness-board.png b/docs/images/auto-update/readiness-board.png
index cbbb0095..60dde5c2 100644
Binary files a/docs/images/auto-update/readiness-board.png and b/docs/images/auto-update/readiness-board.png differ
diff --git a/docs/images/blueprint-model/catalog.png b/docs/images/blueprint-model/catalog.png
index 46279c75..e67e024f 100644
Binary files a/docs/images/blueprint-model/catalog.png and b/docs/images/blueprint-model/catalog.png differ
diff --git a/docs/images/blueprint-model/classification-banner.png b/docs/images/blueprint-model/classification-banner.png
index 4d47b2a1..3997834a 100644
Binary files a/docs/images/blueprint-model/classification-banner.png and b/docs/images/blueprint-model/classification-banner.png differ
diff --git a/docs/images/blueprint-model/detail-sheet.png b/docs/images/blueprint-model/detail-sheet.png
index 9286d1f4..2f0d38c9 100644
Binary files a/docs/images/blueprint-model/detail-sheet.png and b/docs/images/blueprint-model/detail-sheet.png differ
diff --git a/docs/images/blueprint-model/detail-state-review.png b/docs/images/blueprint-model/detail-state-review.png
index 78c100e9..c5f8b5c8 100644
Binary files a/docs/images/blueprint-model/detail-state-review.png and b/docs/images/blueprint-model/detail-state-review.png differ
diff --git a/docs/images/blueprint-model/editor-dialog.png b/docs/images/blueprint-model/editor-dialog.png
index 7b9db2d1..eb7eabf4 100644
Binary files a/docs/images/blueprint-model/editor-dialog.png and b/docs/images/blueprint-model/editor-dialog.png differ
diff --git a/docs/images/blueprint-model/empty-state.png b/docs/images/blueprint-model/empty-state.png
index 0b275b7b..9d3d6199 100644
Binary files a/docs/images/blueprint-model/empty-state.png and b/docs/images/blueprint-model/empty-state.png differ
diff --git a/docs/images/blueprint-model/eviction-dialog.png b/docs/images/blueprint-model/eviction-dialog.png
index e71579cd..97d3a700 100644
Binary files a/docs/images/blueprint-model/eviction-dialog.png and b/docs/images/blueprint-model/eviction-dialog.png differ
diff --git a/docs/images/blueprint-model/eviction-stateless.png b/docs/images/blueprint-model/eviction-stateless.png
index 5f1f8833..ca9465c3 100644
Binary files a/docs/images/blueprint-model/eviction-stateless.png and b/docs/images/blueprint-model/eviction-stateless.png differ
diff --git a/docs/images/blueprint-model/federation-pin.png b/docs/images/blueprint-model/federation-pin.png
index d19d0f65..428d3979 100644
Binary files a/docs/images/blueprint-model/federation-pin.png and b/docs/images/blueprint-model/federation-pin.png differ
diff --git a/docs/images/blueprint-model/node-labels.png b/docs/images/blueprint-model/node-labels.png
index 6f0d2f04..79b3de77 100644
Binary files a/docs/images/blueprint-model/node-labels.png and b/docs/images/blueprint-model/node-labels.png differ
diff --git a/docs/images/blueprint-model/state-review-dialog.png b/docs/images/blueprint-model/state-review-dialog.png
index 0564d3c8..ebf837f3 100644
Binary files a/docs/images/blueprint-model/state-review-dialog.png and b/docs/images/blueprint-model/state-review-dialog.png differ
diff --git a/docs/images/cloud-backup/cloud-backup-active.png b/docs/images/cloud-backup/cloud-backup-active.png
index f6454571..36ffc68a 100644
Binary files a/docs/images/cloud-backup/cloud-backup-active.png and b/docs/images/cloud-backup/cloud-backup-active.png differ
diff --git a/docs/images/cloud-backup/cloud-backup-custom.png b/docs/images/cloud-backup/cloud-backup-custom.png
index 3997bb4b..58a7ba4f 100644
Binary files a/docs/images/cloud-backup/cloud-backup-custom.png and b/docs/images/cloud-backup/cloud-backup-custom.png differ
diff --git a/docs/images/compose-diff-preview/diff-modal.png b/docs/images/compose-diff-preview/diff-modal.png
index 0f281c72..b0f197bf 100644
Binary files a/docs/images/compose-diff-preview/diff-modal.png and b/docs/images/compose-diff-preview/diff-modal.png differ
diff --git a/docs/images/compose-doctor/compose-doctor-findings.png b/docs/images/compose-doctor/compose-doctor-findings.png
index e481894e..5084bfac 100644
Binary files a/docs/images/compose-doctor/compose-doctor-findings.png and b/docs/images/compose-doctor/compose-doctor-findings.png differ
diff --git a/docs/images/compose-doctor/compose-doctor-never-run.png b/docs/images/compose-doctor/compose-doctor-never-run.png
index fba09aa2..a6070dab 100644
Binary files a/docs/images/compose-doctor/compose-doctor-never-run.png and b/docs/images/compose-doctor/compose-doctor-never-run.png differ
diff --git a/docs/images/compose-doctor/compose-doctor-severity-dot.png b/docs/images/compose-doctor/compose-doctor-severity-dot.png
index 78012528..0e31665f 100644
Binary files a/docs/images/compose-doctor/compose-doctor-severity-dot.png and b/docs/images/compose-doctor/compose-doctor-severity-dot.png differ
diff --git a/docs/images/compose-doctor/compose-doctor-tab.png b/docs/images/compose-doctor/compose-doctor-tab.png
index 78012528..bb5bda96 100644
Binary files a/docs/images/compose-doctor/compose-doctor-tab.png and b/docs/images/compose-doctor/compose-doctor-tab.png differ
diff --git a/docs/images/compose-networking/networking-create-dialog.png b/docs/images/compose-networking/networking-create-dialog.png
index 1249a60b..c63d6e6d 100644
Binary files a/docs/images/compose-networking/networking-create-dialog.png and b/docs/images/compose-networking/networking-create-dialog.png differ
diff --git a/docs/images/compose-networking/networking-exposure-intent.png b/docs/images/compose-networking/networking-exposure-intent.png
index 407b0658..072ef5ee 100644
Binary files a/docs/images/compose-networking/networking-exposure-intent.png and b/docs/images/compose-networking/networking-exposure-intent.png differ
diff --git a/docs/images/compose-networking/networking-tab-location.png b/docs/images/compose-networking/networking-tab-location.png
index f5cdcd65..466c0f10 100644
Binary files a/docs/images/compose-networking/networking-tab-location.png and b/docs/images/compose-networking/networking-tab-location.png differ
diff --git a/docs/images/compose-networking/networking-tab-overview.png b/docs/images/compose-networking/networking-tab-overview.png
index 7aa849f7..25d89159 100644
Binary files a/docs/images/compose-networking/networking-tab-overview.png and b/docs/images/compose-networking/networking-tab-overview.png differ
diff --git a/docs/images/compose-storage/storage-node-bound.png b/docs/images/compose-storage/storage-node-bound.png
index 4d5873e5..c937e235 100644
Binary files a/docs/images/compose-storage/storage-node-bound.png and b/docs/images/compose-storage/storage-node-bound.png differ
diff --git a/docs/images/compose-storage/storage-tab.png b/docs/images/compose-storage/storage-tab.png
index 4c5c9712..280bff15 100644
Binary files a/docs/images/compose-storage/storage-tab.png and b/docs/images/compose-storage/storage-tab.png differ
diff --git a/docs/images/cve-suppressions/create-dialog.png b/docs/images/cve-suppressions/create-dialog.png
index 936a72ad..81029048 100644
Binary files a/docs/images/cve-suppressions/create-dialog.png and b/docs/images/cve-suppressions/create-dialog.png differ
diff --git a/docs/images/cve-suppressions/edit-dialog.png b/docs/images/cve-suppressions/edit-dialog.png
new file mode 100644
index 00000000..9cb3288e
Binary files /dev/null and b/docs/images/cve-suppressions/edit-dialog.png differ
diff --git a/docs/images/cve-suppressions/remote-node-banner.png b/docs/images/cve-suppressions/remote-node-banner.png
new file mode 100644
index 00000000..6efa175a
Binary files /dev/null and b/docs/images/cve-suppressions/remote-node-banner.png differ
diff --git a/docs/images/cve-suppressions/settings-panel.png b/docs/images/cve-suppressions/settings-panel.png
index a9f6160c..f629ee90 100644
Binary files a/docs/images/cve-suppressions/settings-panel.png and b/docs/images/cve-suppressions/settings-panel.png differ
diff --git a/docs/images/cve-suppressions/suppressed-row.png b/docs/images/cve-suppressions/suppressed-row.png
index 9a22a9c1..37533480 100644
Binary files a/docs/images/cve-suppressions/suppressed-row.png and b/docs/images/cve-suppressions/suppressed-row.png differ
diff --git a/docs/images/dashboard-dark.png b/docs/images/dashboard-dark.png
index ad45baaa..03ec1268 100644
Binary files a/docs/images/dashboard-dark.png and b/docs/images/dashboard-dark.png differ
diff --git a/docs/images/dashboard-light.png b/docs/images/dashboard-light.png
index 33daebf2..0fcc1e57 100644
Binary files a/docs/images/dashboard-light.png and b/docs/images/dashboard-light.png differ
diff --git a/docs/images/dashboard/configuration-status.png b/docs/images/dashboard/configuration-status.png
index 3514310e..de209edc 100644
Binary files a/docs/images/dashboard/configuration-status.png and b/docs/images/dashboard/configuration-status.png differ
diff --git a/docs/images/dashboard/dashboard-mobile.png b/docs/images/dashboard/dashboard-mobile.png
new file mode 100644
index 00000000..e163d9ea
Binary files /dev/null and b/docs/images/dashboard/dashboard-mobile.png differ
diff --git a/docs/images/dashboard/dashboard-overview.png b/docs/images/dashboard/dashboard-overview.png
index 7bf403f9..196ca05f 100644
Binary files a/docs/images/dashboard/dashboard-overview.png and b/docs/images/dashboard/dashboard-overview.png differ
diff --git a/docs/images/dashboard/fleet-heartbeat.png b/docs/images/dashboard/fleet-heartbeat.png
index ca01c35c..e95e47cf 100644
Binary files a/docs/images/dashboard/fleet-heartbeat.png and b/docs/images/dashboard/fleet-heartbeat.png differ
diff --git a/docs/images/dashboard/recent-alerts.png b/docs/images/dashboard/recent-alerts.png
index 8b3570b3..a220509f 100644
Binary files a/docs/images/dashboard/recent-alerts.png and b/docs/images/dashboard/recent-alerts.png differ
diff --git a/docs/images/dashboard/resource-gauges.png b/docs/images/dashboard/resource-gauges.png
index 5b632b9b..58388c9a 100644
Binary files a/docs/images/dashboard/resource-gauges.png and b/docs/images/dashboard/resource-gauges.png differ
diff --git a/docs/images/dashboard/stack-health.png b/docs/images/dashboard/stack-health.png
index 4f85a6b8..4d974558 100644
Binary files a/docs/images/dashboard/stack-health.png and b/docs/images/dashboard/stack-health.png differ
diff --git a/docs/images/dashboard/status-masthead.png b/docs/images/dashboard/status-masthead.png
index d1c5b047..6fb074e5 100644
Binary files a/docs/images/dashboard/status-masthead.png and b/docs/images/dashboard/status-masthead.png differ
diff --git a/docs/images/deploy-enforcement/block-dialog.png b/docs/images/deploy-enforcement/block-dialog.png
index 0c0388c1..ecbbe916 100644
Binary files a/docs/images/deploy-enforcement/block-dialog.png and b/docs/images/deploy-enforcement/block-dialog.png differ
diff --git a/docs/images/deploy-enforcement/policy-edit-modal.png b/docs/images/deploy-enforcement/policy-edit-modal.png
index 1e93336c..237d4f66 100644
Binary files a/docs/images/deploy-enforcement/policy-edit-modal.png and b/docs/images/deploy-enforcement/policy-edit-modal.png differ
diff --git a/docs/images/deploy-enforcement/policy-list.png b/docs/images/deploy-enforcement/policy-list.png
index 49380236..73f788ee 100644
Binary files a/docs/images/deploy-enforcement/policy-list.png and b/docs/images/deploy-enforcement/policy-list.png differ
diff --git a/docs/images/deploy-progress/inline-banner.png b/docs/images/deploy-progress/inline-banner.png
index da05db94..62ce8e48 100644
Binary files a/docs/images/deploy-progress/inline-banner.png and b/docs/images/deploy-progress/inline-banner.png differ
diff --git a/docs/images/deploy-progress/modal-failed.png b/docs/images/deploy-progress/modal-failed.png
index d4440bd9..349f7b07 100644
Binary files a/docs/images/deploy-progress/modal-failed.png and b/docs/images/deploy-progress/modal-failed.png differ
diff --git a/docs/images/deploy-progress/modal-health-gate.png b/docs/images/deploy-progress/modal-health-gate.png
index abed11ba..76856fd0 100644
Binary files a/docs/images/deploy-progress/modal-health-gate.png and b/docs/images/deploy-progress/modal-health-gate.png differ
diff --git a/docs/images/deploy-progress/modal-raw-output.png b/docs/images/deploy-progress/modal-raw-output.png
index 7de7e38b..5ee6c2c1 100644
Binary files a/docs/images/deploy-progress/modal-raw-output.png and b/docs/images/deploy-progress/modal-raw-output.png differ
diff --git a/docs/images/deploy-progress/modal-streaming.png b/docs/images/deploy-progress/modal-streaming.png
index b2b589bb..23a7e3ad 100644
Binary files a/docs/images/deploy-progress/modal-streaming.png and b/docs/images/deploy-progress/modal-streaming.png differ
diff --git a/docs/images/deploy-progress/modal-succeeded.png b/docs/images/deploy-progress/modal-succeeded.png
index 000c1491..d696b918 100644
Binary files a/docs/images/deploy-progress/modal-succeeded.png and b/docs/images/deploy-progress/modal-succeeded.png differ
diff --git a/docs/images/deploy-progress/pill.png b/docs/images/deploy-progress/pill.png
index bec5a5f1..bb7d7659 100644
Binary files a/docs/images/deploy-progress/pill.png and b/docs/images/deploy-progress/pill.png differ
diff --git a/docs/images/deploy-progress/setting-toggle.png b/docs/images/deploy-progress/setting-toggle.png
index a8694dbb..205c5a64 100644
Binary files a/docs/images/deploy-progress/setting-toggle.png and b/docs/images/deploy-progress/setting-toggle.png differ
diff --git a/docs/images/docker-label-audit/fleet-by-container.png b/docs/images/docker-label-audit/fleet-by-container.png
new file mode 100644
index 00000000..d528f902
Binary files /dev/null and b/docs/images/docker-label-audit/fleet-by-container.png differ
diff --git a/docs/images/docker-label-audit/fleet-by-label-filters.png b/docs/images/docker-label-audit/fleet-by-label-filters.png
new file mode 100644
index 00000000..91de302a
Binary files /dev/null and b/docs/images/docker-label-audit/fleet-by-label-filters.png differ
diff --git a/docs/images/docker-label-audit/fleet-container-expanded.png b/docs/images/docker-label-audit/fleet-container-expanded.png
new file mode 100644
index 00000000..248e6332
Binary files /dev/null and b/docs/images/docker-label-audit/fleet-container-expanded.png differ
diff --git a/docs/images/docker-label-audit/stack-compose-labels.png b/docs/images/docker-label-audit/stack-compose-labels.png
new file mode 100644
index 00000000..5494310e
Binary files /dev/null and b/docs/images/docker-label-audit/stack-compose-labels.png differ
diff --git a/docs/images/editor.png b/docs/images/editor.png
index a2bb5bad..90d855af 100644
Binary files a/docs/images/editor.png and b/docs/images/editor.png differ
diff --git a/docs/images/editor/anatomy-tabs.png b/docs/images/editor/anatomy-tabs.png
index a3a8bbd4..f3b97d61 100644
Binary files a/docs/images/editor/anatomy-tabs.png and b/docs/images/editor/anatomy-tabs.png differ
diff --git a/docs/images/editor/command-center.png b/docs/images/editor/command-center.png
index 7e09af09..a475b8ac 100644
Binary files a/docs/images/editor/command-center.png and b/docs/images/editor/command-center.png differ
diff --git a/docs/images/editor/container-exec-modal.png b/docs/images/editor/container-exec-modal.png
index 09a13398..d6ab0ac4 100644
Binary files a/docs/images/editor/container-exec-modal.png and b/docs/images/editor/container-exec-modal.png differ
diff --git a/docs/images/editor/containers-list.png b/docs/images/editor/containers-list.png
index f0c208cb..e831512d 100644
Binary files a/docs/images/editor/containers-list.png and b/docs/images/editor/containers-list.png differ
diff --git a/docs/images/editor/editor-edit-mode.png b/docs/images/editor/editor-edit-mode.png
index a52d00dc..db714a89 100644
Binary files a/docs/images/editor/editor-edit-mode.png and b/docs/images/editor/editor-edit-mode.png differ
diff --git a/docs/images/editor/editor-mobile.png b/docs/images/editor/editor-mobile.png
index 56b334cc..0c588d34 100644
Binary files a/docs/images/editor/editor-mobile.png and b/docs/images/editor/editor-mobile.png differ
diff --git a/docs/images/editor/editor-overview.png b/docs/images/editor/editor-overview.png
index c8cb4b88..1d43d347 100644
Binary files a/docs/images/editor/editor-overview.png and b/docs/images/editor/editor-overview.png differ
diff --git a/docs/images/editor/scan-status-banner.png b/docs/images/editor/scan-status-banner.png
new file mode 100644
index 00000000..2c8c68b9
Binary files /dev/null and b/docs/images/editor/scan-status-banner.png differ
diff --git a/docs/images/environment-guardrails/env-overview.png b/docs/images/environment-guardrails/env-overview.png
index 8ac3ef99..74e487cf 100644
Binary files a/docs/images/environment-guardrails/env-overview.png and b/docs/images/environment-guardrails/env-overview.png differ
diff --git a/docs/images/environment-guardrails/env-panel.png b/docs/images/environment-guardrails/env-panel.png
deleted file mode 100644
index def7a991..00000000
Binary files a/docs/images/environment-guardrails/env-panel.png and /dev/null differ
diff --git a/docs/images/environment-guardrails/env-secrets.png b/docs/images/environment-guardrails/env-secrets.png
index 123d4ef1..da9fcba3 100644
Binary files a/docs/images/environment-guardrails/env-secrets.png and b/docs/images/environment-guardrails/env-secrets.png differ
diff --git a/docs/images/environment-guardrails/settings-deploy-guardrails.png b/docs/images/environment-guardrails/settings-deploy-guardrails.png
index d1c118bb..a3a3f4a7 100644
Binary files a/docs/images/environment-guardrails/settings-deploy-guardrails.png and b/docs/images/environment-guardrails/settings-deploy-guardrails.png differ
diff --git a/docs/images/fleet-actions/fleet-actions-bulk-assign.png b/docs/images/fleet-actions/fleet-actions-bulk-assign.png
deleted file mode 100644
index 9c442ec9..00000000
Binary files a/docs/images/fleet-actions/fleet-actions-bulk-assign.png and /dev/null differ
diff --git a/docs/images/fleet-actions/fleet-actions-overview.png b/docs/images/fleet-actions/fleet-actions-overview.png
index 5c71013d..94e49fc9 100644
Binary files a/docs/images/fleet-actions/fleet-actions-overview.png and b/docs/images/fleet-actions/fleet-actions-overview.png differ
diff --git a/docs/images/fleet-actions/fleet-actions-preview-states.png b/docs/images/fleet-actions/fleet-actions-preview-states.png
new file mode 100644
index 00000000..c5add8aa
Binary files /dev/null and b/docs/images/fleet-actions/fleet-actions-preview-states.png differ
diff --git a/docs/images/fleet-actions/fleet-actions-prune.png b/docs/images/fleet-actions/fleet-actions-prune.png
index fe69ae0a..efc18cd2 100644
Binary files a/docs/images/fleet-actions/fleet-actions-prune.png and b/docs/images/fleet-actions/fleet-actions-prune.png differ
diff --git a/docs/images/fleet-actions/fleet-actions-stop-confirm.png b/docs/images/fleet-actions/fleet-actions-stop-confirm.png
index 4a35afd6..bfa9a4ba 100644
Binary files a/docs/images/fleet-actions/fleet-actions-stop-confirm.png and b/docs/images/fleet-actions/fleet-actions-stop-confirm.png differ
diff --git a/docs/images/fleet-actions/fleet-actions-stop-results.png b/docs/images/fleet-actions/fleet-actions-stop-results.png
index c4bcb7ba..005566ad 100644
Binary files a/docs/images/fleet-actions/fleet-actions-stop-results.png and b/docs/images/fleet-actions/fleet-actions-stop-results.png differ
diff --git a/docs/images/fleet-backups/browse-snapshots.png b/docs/images/fleet-backups/browse-snapshots.png
index e63c4eab..408cf55e 100644
Binary files a/docs/images/fleet-backups/browse-snapshots.png and b/docs/images/fleet-backups/browse-snapshots.png differ
diff --git a/docs/images/fleet-backups/create-snapshot.png b/docs/images/fleet-backups/create-snapshot.png
index b44fc5bc..3c91e8a4 100644
Binary files a/docs/images/fleet-backups/create-snapshot.png and b/docs/images/fleet-backups/create-snapshot.png differ
diff --git a/docs/images/fleet-backups/restore-dialog.png b/docs/images/fleet-backups/restore-dialog.png
index 3a04e628..523dd99b 100644
Binary files a/docs/images/fleet-backups/restore-dialog.png and b/docs/images/fleet-backups/restore-dialog.png differ
diff --git a/docs/images/fleet-backups/snapshot-detail.png b/docs/images/fleet-backups/snapshot-detail.png
index c8220863..c4a36010 100644
Binary files a/docs/images/fleet-backups/snapshot-detail.png and b/docs/images/fleet-backups/snapshot-detail.png differ
diff --git a/docs/images/fleet-dossier/fleet-dossier-toolbar.png b/docs/images/fleet-dossier/fleet-dossier-toolbar.png
new file mode 100644
index 00000000..a8cc0773
Binary files /dev/null and b/docs/images/fleet-dossier/fleet-dossier-toolbar.png differ
diff --git a/docs/images/fleet-federation/cordon-confirm-dialog.png b/docs/images/fleet-federation/cordon-confirm-dialog.png
new file mode 100644
index 00000000..78dc9122
Binary files /dev/null and b/docs/images/fleet-federation/cordon-confirm-dialog.png differ
diff --git a/docs/images/fleet-federation/cordon-modal-with-reason.png b/docs/images/fleet-federation/cordon-modal-with-reason.png
deleted file mode 100644
index 3443ea15..00000000
Binary files a/docs/images/fleet-federation/cordon-modal-with-reason.png and /dev/null differ
diff --git a/docs/images/fleet-federation/federation-tab-overview.png b/docs/images/fleet-federation/federation-tab-overview.png
new file mode 100644
index 00000000..cd6d8735
Binary files /dev/null and b/docs/images/fleet-federation/federation-tab-overview.png differ
diff --git a/docs/images/fleet-federation/federation-tab.png b/docs/images/fleet-federation/federation-tab.png
deleted file mode 100644
index e0168d0f..00000000
Binary files a/docs/images/fleet-federation/federation-tab.png and /dev/null differ
diff --git a/docs/images/fleet-federation/node-card-cordoned-badge.png b/docs/images/fleet-federation/node-card-cordoned-badge.png
index 2cf7db6c..e04ba85c 100644
Binary files a/docs/images/fleet-federation/node-card-cordoned-badge.png and b/docs/images/fleet-federation/node-card-cordoned-badge.png differ
diff --git a/docs/images/fleet-federation/node-card-kebab-cordon.png b/docs/images/fleet-federation/node-card-kebab-cordon.png
deleted file mode 100644
index 42f06824..00000000
Binary files a/docs/images/fleet-federation/node-card-kebab-cordon.png and /dev/null differ
diff --git a/docs/images/fleet-federation/pin-applied-toast.png b/docs/images/fleet-federation/pin-applied-toast.png
new file mode 100644
index 00000000..1c9b1903
Binary files /dev/null and b/docs/images/fleet-federation/pin-applied-toast.png differ
diff --git a/docs/images/fleet-federation/pin-dropdown-cordoned-hint.png b/docs/images/fleet-federation/pin-dropdown-cordoned-hint.png
new file mode 100644
index 00000000..21f98818
Binary files /dev/null and b/docs/images/fleet-federation/pin-dropdown-cordoned-hint.png differ
diff --git a/docs/images/fleet-federation/rollout-preview-blocked.png b/docs/images/fleet-federation/rollout-preview-blocked.png
new file mode 100644
index 00000000..a59ea4d1
Binary files /dev/null and b/docs/images/fleet-federation/rollout-preview-blocked.png differ
diff --git a/docs/images/fleet-federation/rollout-preview-safe.png b/docs/images/fleet-federation/rollout-preview-safe.png
new file mode 100644
index 00000000..4927b5f6
Binary files /dev/null and b/docs/images/fleet-federation/rollout-preview-safe.png differ
diff --git a/docs/images/fleet-sync/control-security.png b/docs/images/fleet-sync/control-security.png
deleted file mode 100644
index 09fb70d4..00000000
Binary files a/docs/images/fleet-sync/control-security.png and /dev/null differ
diff --git a/docs/images/fleet-sync/fleet-sync-policies-control.png b/docs/images/fleet-sync/fleet-sync-policies-control.png
new file mode 100644
index 00000000..1e7ce17b
Binary files /dev/null and b/docs/images/fleet-sync/fleet-sync-policies-control.png differ
diff --git a/docs/images/fleet-sync/fleet-sync-policies-replica.png b/docs/images/fleet-sync/fleet-sync-policies-replica.png
new file mode 100644
index 00000000..895b763f
Binary files /dev/null and b/docs/images/fleet-sync/fleet-sync-policies-replica.png differ
diff --git a/docs/images/fleet-sync/fleet-sync-proxy-notice.png b/docs/images/fleet-sync/fleet-sync-proxy-notice.png
new file mode 100644
index 00000000..2ec30ec3
Binary files /dev/null and b/docs/images/fleet-sync/fleet-sync-proxy-notice.png differ
diff --git a/docs/images/fleet-sync/fleet-sync-suppressions-replica.png b/docs/images/fleet-sync/fleet-sync-suppressions-replica.png
new file mode 100644
index 00000000..62681853
Binary files /dev/null and b/docs/images/fleet-sync/fleet-sync-suppressions-replica.png differ
diff --git a/docs/images/fleet-sync/remote-security-via-proxy.png b/docs/images/fleet-sync/remote-security-via-proxy.png
deleted file mode 100644
index 663e901f..00000000
Binary files a/docs/images/fleet-sync/remote-security-via-proxy.png and /dev/null differ
diff --git a/docs/images/fleet-view/fleet-drill-down.png b/docs/images/fleet-view/fleet-drill-down.png
index 0e9d4413..24e4a196 100644
Binary files a/docs/images/fleet-view/fleet-drill-down.png and b/docs/images/fleet-view/fleet-drill-down.png differ
diff --git a/docs/images/fleet-view/fleet-node-updates.png b/docs/images/fleet-view/fleet-node-updates.png
index 2bed33c0..e03ca83c 100644
Binary files a/docs/images/fleet-view/fleet-node-updates.png and b/docs/images/fleet-view/fleet-node-updates.png differ
diff --git a/docs/images/fleet-view/fleet-overview.png b/docs/images/fleet-view/fleet-overview.png
index 6e8e3d0f..306f0519 100644
Binary files a/docs/images/fleet-view/fleet-overview.png and b/docs/images/fleet-view/fleet-overview.png differ
diff --git a/docs/images/fleet-view/fleet-status-tab.png b/docs/images/fleet-view/fleet-status-tab.png
index ad4de54d..bc9a3549 100644
Binary files a/docs/images/fleet-view/fleet-status-tab.png and b/docs/images/fleet-view/fleet-status-tab.png differ
diff --git a/docs/images/fleet-view/fleet-topology.png b/docs/images/fleet-view/fleet-topology.png
index e878b392..102ddd57 100644
Binary files a/docs/images/fleet-view/fleet-topology.png and b/docs/images/fleet-view/fleet-topology.png differ
diff --git a/docs/images/fleet-view/local-update-confirm.png b/docs/images/fleet-view/local-update-confirm.png
deleted file mode 100644
index 08e19f5d..00000000
Binary files a/docs/images/fleet-view/local-update-confirm.png and /dev/null differ
diff --git a/docs/images/fleet-view/node-card-opsix-online.png b/docs/images/fleet-view/node-card-opsix-online.png
new file mode 100644
index 00000000..f9c7c951
Binary files /dev/null and b/docs/images/fleet-view/node-card-opsix-online.png differ
diff --git a/docs/images/fleet-view/node-card-update-available.png b/docs/images/fleet-view/node-card-update-available.png
deleted file mode 100644
index 5b94fd65..00000000
Binary files a/docs/images/fleet-view/node-card-update-available.png and /dev/null differ
diff --git a/docs/images/fleet.png b/docs/images/fleet.png
index b3533363..54ffedc2 100644
Binary files a/docs/images/fleet.png and b/docs/images/fleet.png differ
diff --git a/docs/images/global-observability/error-only-filter.png b/docs/images/global-observability/error-only-filter.png
index 60ae4897..3bb1b446 100644
Binary files a/docs/images/global-observability/error-only-filter.png and b/docs/images/global-observability/error-only-filter.png differ
diff --git a/docs/images/global-observability/feed-bands.png b/docs/images/global-observability/feed-bands.png
index 645dc39f..7d98c6b5 100644
Binary files a/docs/images/global-observability/feed-bands.png and b/docs/images/global-observability/feed-bands.png differ
diff --git a/docs/images/global-observability/filter-strip.png b/docs/images/global-observability/filter-strip.png
index 5f37ea03..2d2c0d83 100644
Binary files a/docs/images/global-observability/filter-strip.png and b/docs/images/global-observability/filter-strip.png differ
diff --git a/docs/images/global-observability/global-observability-overview.png b/docs/images/global-observability/global-observability-overview.png
index 049f3135..1f683ce1 100644
Binary files a/docs/images/global-observability/global-observability-overview.png and b/docs/images/global-observability/global-observability-overview.png differ
diff --git a/docs/images/global-observability/masthead.png b/docs/images/global-observability/masthead.png
index 18ff9c4a..0d2aeb94 100644
Binary files a/docs/images/global-observability/masthead.png and b/docs/images/global-observability/masthead.png differ
diff --git a/docs/images/global-observability/paused-resume-chip.png b/docs/images/global-observability/paused-resume-chip.png
index e076c23a..a2f9f05d 100644
Binary files a/docs/images/global-observability/paused-resume-chip.png and b/docs/images/global-observability/paused-resume-chip.png differ
diff --git a/docs/images/global-observability/signal-rail.png b/docs/images/global-observability/signal-rail.png
index 90dde583..d73abf49 100644
Binary files a/docs/images/global-observability/signal-rail.png and b/docs/images/global-observability/signal-rail.png differ
diff --git a/docs/images/global-search/mobile-more-menu.png b/docs/images/global-search/mobile-more-menu.png
new file mode 100644
index 00000000..67e7c19f
Binary files /dev/null and b/docs/images/global-search/mobile-more-menu.png differ
diff --git a/docs/images/global-search/mobile-palette.png b/docs/images/global-search/mobile-palette.png
new file mode 100644
index 00000000..4d8c1a22
Binary files /dev/null and b/docs/images/global-search/mobile-palette.png differ
diff --git a/docs/images/global-search/palette-nodes.png b/docs/images/global-search/palette-nodes.png
index 7fa198d9..178f35a5 100644
Binary files a/docs/images/global-search/palette-nodes.png and b/docs/images/global-search/palette-nodes.png differ
diff --git a/docs/images/global-search/palette-pages.png b/docs/images/global-search/palette-pages.png
index 17775a1a..10d4c0a3 100644
Binary files a/docs/images/global-search/palette-pages.png and b/docs/images/global-search/palette-pages.png differ
diff --git a/docs/images/global-search/palette-stacks.png b/docs/images/global-search/palette-stacks.png
index a7cae5cb..83f02cd1 100644
Binary files a/docs/images/global-search/palette-stacks.png and b/docs/images/global-search/palette-stacks.png differ
diff --git a/docs/images/health-gated-updates/dossier-rollback-readiness.png b/docs/images/health-gated-updates/dossier-rollback-readiness.png
index ea28151f..59e7a65a 100644
Binary files a/docs/images/health-gated-updates/dossier-rollback-readiness.png and b/docs/images/health-gated-updates/dossier-rollback-readiness.png differ
diff --git a/docs/images/health-gated-updates/modal-gate-failed.png b/docs/images/health-gated-updates/modal-gate-failed.png
index df549ee3..05fe2701 100644
Binary files a/docs/images/health-gated-updates/modal-gate-failed.png and b/docs/images/health-gated-updates/modal-gate-failed.png differ
diff --git a/docs/images/health-gated-updates/modal-verifying.png b/docs/images/health-gated-updates/modal-verifying.png
index f2cd5c36..76856fd0 100644
Binary files a/docs/images/health-gated-updates/modal-verifying.png and b/docs/images/health-gated-updates/modal-verifying.png differ
diff --git a/docs/images/health-gated-updates/readiness-dialog.png b/docs/images/health-gated-updates/readiness-dialog.png
index 57d636c5..238c89ab 100644
Binary files a/docs/images/health-gated-updates/readiness-dialog.png and b/docs/images/health-gated-updates/readiness-dialog.png differ
diff --git a/docs/images/health-gated-updates/settings.png b/docs/images/health-gated-updates/settings.png
index b9599d8e..0a5b1108 100644
Binary files a/docs/images/health-gated-updates/settings.png and b/docs/images/health-gated-updates/settings.png differ
diff --git a/docs/images/introduction/fleet-overview.png b/docs/images/introduction/fleet-overview.png
index cfbdc24c..0a602b9b 100644
Binary files a/docs/images/introduction/fleet-overview.png and b/docs/images/introduction/fleet-overview.png differ
diff --git a/docs/images/introduction/home-overview.png b/docs/images/introduction/home-overview.png
index d6ba8eb1..9347bdb1 100644
Binary files a/docs/images/introduction/home-overview.png and b/docs/images/introduction/home-overview.png differ
diff --git a/docs/images/introduction/resources-overview.png b/docs/images/introduction/resources-overview.png
index 0027ae12..1cf0c7d1 100644
Binary files a/docs/images/introduction/resources-overview.png and b/docs/images/introduction/resources-overview.png differ
diff --git a/docs/images/introduction/security-overview.png b/docs/images/introduction/security-overview.png
index b229943b..4e96c150 100644
Binary files a/docs/images/introduction/security-overview.png and b/docs/images/introduction/security-overview.png differ
diff --git a/docs/images/introduction/stack-workspace.png b/docs/images/introduction/stack-workspace.png
index 0a215f6c..0d5de318 100644
Binary files a/docs/images/introduction/stack-workspace.png and b/docs/images/introduction/stack-workspace.png differ
diff --git a/docs/images/licensing/license-activate-section.png b/docs/images/licensing/license-activate-section.png
index ab03aaf6..dab62867 100644
Binary files a/docs/images/licensing/license-activate-section.png and b/docs/images/licensing/license-activate-section.png differ
diff --git a/docs/images/licensing/license-admiral-active.png b/docs/images/licensing/license-admiral-active.png
index 951250ea..fc283580 100644
Binary files a/docs/images/licensing/license-admiral-active.png and b/docs/images/licensing/license-admiral-active.png differ
diff --git a/docs/images/licensing/license-community.png b/docs/images/licensing/license-community.png
index 1ab36293..017effa2 100644
Binary files a/docs/images/licensing/license-community.png and b/docs/images/licensing/license-community.png differ
diff --git a/docs/images/licensing/profile-menu.png b/docs/images/licensing/profile-menu.png
index 2aeb9f27..48094d26 100644
Binary files a/docs/images/licensing/profile-menu.png and b/docs/images/licensing/profile-menu.png differ
diff --git a/docs/images/logs.png b/docs/images/logs.png
index 0f45637b..f5ba346b 100644
Binary files a/docs/images/logs.png and b/docs/images/logs.png differ
diff --git a/docs/images/multi-node/add-node-pilot.png b/docs/images/multi-node/add-node-pilot.png
index 671728a5..f270e740 100644
Binary files a/docs/images/multi-node/add-node-pilot.png and b/docs/images/multi-node/add-node-pilot.png differ
diff --git a/docs/images/multi-node/add-node-proxy.png b/docs/images/multi-node/add-node-proxy.png
index f2ff0919..b2c640f9 100644
Binary files a/docs/images/multi-node/add-node-proxy.png and b/docs/images/multi-node/add-node-proxy.png differ
diff --git a/docs/images/multi-node/edit-pilot-regenerate.png b/docs/images/multi-node/edit-pilot-regenerate.png
index d25806bc..93f7bb40 100644
Binary files a/docs/images/multi-node/edit-pilot-regenerate.png and b/docs/images/multi-node/edit-pilot-regenerate.png differ
diff --git a/docs/images/multi-node/generate-token.png b/docs/images/multi-node/generate-token.png
index c78d9b60..24b40856 100644
Binary files a/docs/images/multi-node/generate-token.png and b/docs/images/multi-node/generate-token.png differ
diff --git a/docs/images/multi-node/node-manager.png b/docs/images/multi-node/node-manager.png
index 6fb03635..2cb8055d 100644
Binary files a/docs/images/multi-node/node-manager.png and b/docs/images/multi-node/node-manager.png differ
diff --git a/docs/images/multi-node/node-switcher-dropdown.png b/docs/images/multi-node/node-switcher-dropdown.png
index 0a3fe619..dedc8da3 100644
Binary files a/docs/images/multi-node/node-switcher-dropdown.png and b/docs/images/multi-node/node-switcher-dropdown.png differ
diff --git a/docs/images/multi-node/nodes-table-overview.png b/docs/images/multi-node/nodes-table-overview.png
index 6f1ff2fd..59b0f4d7 100644
Binary files a/docs/images/multi-node/nodes-table-overview.png and b/docs/images/multi-node/nodes-table-overview.png differ
diff --git a/docs/images/multi-node/pilot-enroll.png b/docs/images/multi-node/pilot-enroll.png
index 59303aa9..68a5366c 100644
Binary files a/docs/images/multi-node/pilot-enroll.png and b/docs/images/multi-node/pilot-enroll.png differ
diff --git a/docs/images/networks/create-network.png b/docs/images/networks/create-network.png
deleted file mode 100644
index c030c9d3..00000000
Binary files a/docs/images/networks/create-network.png and /dev/null differ
diff --git a/docs/images/networks/network-inspect.png b/docs/images/networks/network-inspect.png
deleted file mode 100644
index 0217157d..00000000
Binary files a/docs/images/networks/network-inspect.png and /dev/null differ
diff --git a/docs/images/networks/network-topology-toggle.png b/docs/images/networks/network-topology-toggle.png
deleted file mode 100644
index 2966cee7..00000000
Binary files a/docs/images/networks/network-topology-toggle.png and /dev/null differ
diff --git a/docs/images/networks/network-topology.png b/docs/images/networks/network-topology.png
deleted file mode 100644
index 529055f0..00000000
Binary files a/docs/images/networks/network-topology.png and /dev/null differ
diff --git a/docs/images/networks/networks-list.png b/docs/images/networks/networks-list.png
deleted file mode 100644
index e76f138f..00000000
Binary files a/docs/images/networks/networks-list.png and /dev/null differ
diff --git a/docs/images/node-compatibility/connection-test-version.png b/docs/images/node-compatibility/connection-test-version.png
index 78b27dd4..dc9640d9 100644
Binary files a/docs/images/node-compatibility/connection-test-version.png and b/docs/images/node-compatibility/connection-test-version.png differ
diff --git a/docs/images/node-compatibility/lock-card.png b/docs/images/node-compatibility/lock-card.png
index b0815afc..c4c5f6f3 100644
Binary files a/docs/images/node-compatibility/lock-card.png and b/docs/images/node-compatibility/lock-card.png differ
diff --git a/docs/images/node-compatibility/node-switcher-versions.png b/docs/images/node-compatibility/node-switcher-versions.png
index 563ec8a0..84059fd4 100644
Binary files a/docs/images/node-compatibility/node-switcher-versions.png and b/docs/images/node-compatibility/node-switcher-versions.png differ
diff --git a/docs/images/overview/compose-doctor.png b/docs/images/overview/compose-doctor.png
index 99cf8d26..22ba285c 100644
Binary files a/docs/images/overview/compose-doctor.png and b/docs/images/overview/compose-doctor.png differ
diff --git a/docs/images/overview/scheduled-operations.png b/docs/images/overview/scheduled-operations.png
index 58945bd9..08929f0d 100644
Binary files a/docs/images/overview/scheduled-operations.png and b/docs/images/overview/scheduled-operations.png differ
diff --git a/docs/images/overview/security-overview.png b/docs/images/overview/security-overview.png
index 508da2dd..7aa1c472 100644
Binary files a/docs/images/overview/security-overview.png and b/docs/images/overview/security-overview.png differ
diff --git a/docs/images/pilot-agent/01-add-node-pilot.png b/docs/images/pilot-agent/01-add-node-pilot.png
index 8c6c3012..4843d3e8 100644
Binary files a/docs/images/pilot-agent/01-add-node-pilot.png and b/docs/images/pilot-agent/01-add-node-pilot.png differ
diff --git a/docs/images/pilot-agent/02-enrollment-dialog.png b/docs/images/pilot-agent/02-enrollment-dialog.png
index c90def62..0a75b0a3 100644
Binary files a/docs/images/pilot-agent/02-enrollment-dialog.png and b/docs/images/pilot-agent/02-enrollment-dialog.png differ
diff --git a/docs/images/pilot-agent/03-nodes-table-tunnel.png b/docs/images/pilot-agent/03-nodes-table-tunnel.png
index ac0170f2..70b23959 100644
Binary files a/docs/images/pilot-agent/03-nodes-table-tunnel.png and b/docs/images/pilot-agent/03-nodes-table-tunnel.png differ
diff --git a/docs/images/pilot-agent/04-regenerate-token.png b/docs/images/pilot-agent/04-regenerate-token.png
index 07ff64b3..8682af50 100644
Binary files a/docs/images/pilot-agent/04-regenerate-token.png and b/docs/images/pilot-agent/04-regenerate-token.png differ
diff --git a/docs/images/quickstart/dashboard.png b/docs/images/quickstart/dashboard.png
index f7ebe3bf..f32c068d 100644
Binary files a/docs/images/quickstart/dashboard.png and b/docs/images/quickstart/dashboard.png differ
diff --git a/docs/images/quickstart/setup-cold-start.png b/docs/images/quickstart/setup-cold-start.png
index f3858647..4c387023 100644
Binary files a/docs/images/quickstart/setup-cold-start.png and b/docs/images/quickstart/setup-cold-start.png differ
diff --git a/docs/images/quickstart/setup-environment.png b/docs/images/quickstart/setup-environment.png
index afa49b25..081b077f 100644
Binary files a/docs/images/quickstart/setup-environment.png and b/docs/images/quickstart/setup-environment.png differ
diff --git a/docs/images/resources/resources-image-inspect.png b/docs/images/resources/resources-image-inspect.png
new file mode 100644
index 00000000..d2140bf9
Binary files /dev/null and b/docs/images/resources/resources-image-inspect.png differ
diff --git a/docs/images/resources/resources-reclaim.png b/docs/images/resources/resources-reclaim.png
index b50155a1..f78f8420 100644
Binary files a/docs/images/resources/resources-reclaim.png and b/docs/images/resources/resources-reclaim.png differ
diff --git a/docs/images/resources/resources-unmanaged-tab.png b/docs/images/resources/resources-unmanaged-tab.png
index 9fee55c8..395cb0f1 100644
Binary files a/docs/images/resources/resources-unmanaged-tab.png and b/docs/images/resources/resources-unmanaged-tab.png differ
diff --git a/docs/images/resources/resources-volumes-tab.png b/docs/images/resources/resources-volumes-tab.png
index a21f262e..cc1892a8 100644
Binary files a/docs/images/resources/resources-volumes-tab.png and b/docs/images/resources/resources-volumes-tab.png differ
diff --git a/docs/images/scheduled-operations/action-picker.png b/docs/images/scheduled-operations/action-picker.png
index 08179dc4..36889c8b 100644
Binary files a/docs/images/scheduled-operations/action-picker.png and b/docs/images/scheduled-operations/action-picker.png differ
diff --git a/docs/images/scheduled-operations/all-tasks.png b/docs/images/scheduled-operations/all-tasks.png
index 34a88528..e20da562 100644
Binary files a/docs/images/scheduled-operations/all-tasks.png and b/docs/images/scheduled-operations/all-tasks.png differ
diff --git a/docs/images/scheduled-operations/create-prune.png b/docs/images/scheduled-operations/create-prune.png
index 4f955d8f..3d42dcb6 100644
Binary files a/docs/images/scheduled-operations/create-prune.png and b/docs/images/scheduled-operations/create-prune.png differ
diff --git a/docs/images/scheduled-operations/create-restart.png b/docs/images/scheduled-operations/create-restart.png
index 69b2eccc..18132544 100644
Binary files a/docs/images/scheduled-operations/create-restart.png and b/docs/images/scheduled-operations/create-restart.png differ
diff --git a/docs/images/scheduled-operations/create-scan.png b/docs/images/scheduled-operations/create-scan.png
index 9d026c8f..f1ed551d 100644
Binary files a/docs/images/scheduled-operations/create-scan.png and b/docs/images/scheduled-operations/create-scan.png differ
diff --git a/docs/images/scheduled-operations/run-history.png b/docs/images/scheduled-operations/run-history.png
index b0a7a046..661be634 100644
Binary files a/docs/images/scheduled-operations/run-history.png and b/docs/images/scheduled-operations/run-history.png differ
diff --git a/docs/images/scheduled-operations/timeline.png b/docs/images/scheduled-operations/timeline.png
index c186c69f..9e90b67a 100644
Binary files a/docs/images/scheduled-operations/timeline.png and b/docs/images/scheduled-operations/timeline.png differ
diff --git a/docs/images/security/add-policy.png b/docs/images/security/add-policy.png
new file mode 100644
index 00000000..74225a7a
Binary files /dev/null and b/docs/images/security/add-policy.png differ
diff --git a/docs/images/security/compose-risks.png b/docs/images/security/compose-risks.png
new file mode 100644
index 00000000..c4fb1700
Binary files /dev/null and b/docs/images/security/compose-risks.png differ
diff --git a/docs/images/security/history.png b/docs/images/security/history.png
new file mode 100644
index 00000000..5e16b07b
Binary files /dev/null and b/docs/images/security/history.png differ
diff --git a/docs/images/security/images-tab.png b/docs/images/security/images-tab.png
new file mode 100644
index 00000000..d4d5d6e5
Binary files /dev/null and b/docs/images/security/images-tab.png differ
diff --git a/docs/images/security/mobile.png b/docs/images/security/mobile.png
new file mode 100644
index 00000000..705a2b77
Binary files /dev/null and b/docs/images/security/mobile.png differ
diff --git a/docs/images/security/overview.png b/docs/images/security/overview.png
new file mode 100644
index 00000000..4a38381a
Binary files /dev/null and b/docs/images/security/overview.png differ
diff --git a/docs/images/security/scan-report.png b/docs/images/security/scan-report.png
new file mode 100644
index 00000000..0dc43ac7
Binary files /dev/null and b/docs/images/security/scan-report.png differ
diff --git a/docs/images/security/scanner-setup.png b/docs/images/security/scanner-setup.png
new file mode 100644
index 00000000..044df4b9
Binary files /dev/null and b/docs/images/security/scanner-setup.png differ
diff --git a/docs/images/security/secrets.png b/docs/images/security/secrets.png
new file mode 100644
index 00000000..c62d15e4
Binary files /dev/null and b/docs/images/security/secrets.png differ
diff --git a/docs/images/security/suppressions.png b/docs/images/security/suppressions.png
new file mode 100644
index 00000000..14b4057d
Binary files /dev/null and b/docs/images/security/suppressions.png differ
diff --git a/docs/images/settings/appearance-density.png b/docs/images/settings/appearance-density.png
index 486cb18d..c4c5cf55 100644
Binary files a/docs/images/settings/appearance-density.png and b/docs/images/settings/appearance-density.png differ
diff --git a/docs/images/settings/appearance-display.png b/docs/images/settings/appearance-display.png
index 4a3570b6..7c99645d 100644
Binary files a/docs/images/settings/appearance-display.png and b/docs/images/settings/appearance-display.png differ
diff --git a/docs/images/settings/appearance-motion-effects.png b/docs/images/settings/appearance-motion-effects.png
new file mode 100644
index 00000000..c7713a83
Binary files /dev/null and b/docs/images/settings/appearance-motion-effects.png differ
diff --git a/docs/images/settings/appearance-overview.png b/docs/images/settings/appearance-overview.png
index e3ffcfcd..c2795687 100644
Binary files a/docs/images/settings/appearance-overview.png and b/docs/images/settings/appearance-overview.png differ
diff --git a/docs/images/settings/appearance-quick-switch.png b/docs/images/settings/appearance-quick-switch.png
index 506ab07b..c1356307 100644
Binary files a/docs/images/settings/appearance-quick-switch.png and b/docs/images/settings/appearance-quick-switch.png differ
diff --git a/docs/images/settings/appearance-typography.png b/docs/images/settings/appearance-typography.png
index c6f497ff..f28de0e1 100644
Binary files a/docs/images/settings/appearance-typography.png and b/docs/images/settings/appearance-typography.png differ
diff --git a/docs/images/settings/settings-account-2fa.png b/docs/images/settings/settings-account-2fa.png
index 2f70c618..397c3a1e 100644
Binary files a/docs/images/settings/settings-account-2fa.png and b/docs/images/settings/settings-account-2fa.png differ
diff --git a/docs/images/settings/settings-cloud-backup.png b/docs/images/settings/settings-cloud-backup.png
index 63b32d72..b350ec69 100644
Binary files a/docs/images/settings/settings-cloud-backup.png and b/docs/images/settings/settings-cloud-backup.png differ
diff --git a/docs/images/settings/settings-container-alerts.png b/docs/images/settings/settings-container-alerts.png
index 271f4bfd..56d9dc40 100644
Binary files a/docs/images/settings/settings-container-alerts.png and b/docs/images/settings/settings-container-alerts.png differ
diff --git a/docs/images/settings/settings-fleet.png b/docs/images/settings/settings-fleet.png
index 5759152e..7b169f7c 100644
Binary files a/docs/images/settings/settings-fleet.png and b/docs/images/settings/settings-fleet.png differ
diff --git a/docs/images/settings/settings-host-alerts.png b/docs/images/settings/settings-host-alerts.png
index 8c343b9d..a6c0c461 100644
Binary files a/docs/images/settings/settings-host-alerts.png and b/docs/images/settings/settings-host-alerts.png differ
diff --git a/docs/images/settings/settings-hub-grouped.png b/docs/images/settings/settings-hub-grouped.png
index 9aec6dbc..e5debf4b 100644
Binary files a/docs/images/settings/settings-hub-grouped.png and b/docs/images/settings/settings-hub-grouped.png differ
diff --git a/docs/images/settings/settings-image-updates.png b/docs/images/settings/settings-image-updates.png
index e2ab303e..fe6d5ad4 100644
Binary files a/docs/images/settings/settings-image-updates.png and b/docs/images/settings/settings-image-updates.png differ
diff --git a/docs/images/settings/settings-license.png b/docs/images/settings/settings-license.png
index 8ca8054f..94f5f1f5 100644
Binary files a/docs/images/settings/settings-license.png and b/docs/images/settings/settings-license.png differ
diff --git a/docs/images/settings/settings-node-scope.png b/docs/images/settings/settings-node-scope.png
index 9ec3a373..a6c0c461 100644
Binary files a/docs/images/settings/settings-node-scope.png and b/docs/images/settings/settings-node-scope.png differ
diff --git a/docs/images/settings/settings-recovery.png b/docs/images/settings/settings-recovery.png
index e8a62e85..0a792cb3 100644
Binary files a/docs/images/settings/settings-recovery.png and b/docs/images/settings/settings-recovery.png differ
diff --git a/docs/images/settings/settings-search.png b/docs/images/settings/settings-search.png
index 29862a91..1d47864e 100644
Binary files a/docs/images/settings/settings-search.png and b/docs/images/settings/settings-search.png differ
diff --git a/docs/images/settings/settings-stacks-guardrails.png b/docs/images/settings/settings-stacks-guardrails.png
index 0b67a155..ced84a0d 100644
Binary files a/docs/images/settings/settings-stacks-guardrails.png and b/docs/images/settings/settings-stacks-guardrails.png differ
diff --git a/docs/images/sidebar/sidebar-activity.png b/docs/images/sidebar/sidebar-activity.png
index 12653b44..947794a7 100644
Binary files a/docs/images/sidebar/sidebar-activity.png and b/docs/images/sidebar/sidebar-activity.png differ
diff --git a/docs/images/sidebar/sidebar-bulk-mode.png b/docs/images/sidebar/sidebar-bulk-mode.png
index f19eb362..22af2db9 100644
Binary files a/docs/images/sidebar/sidebar-bulk-mode.png and b/docs/images/sidebar/sidebar-bulk-mode.png differ
diff --git a/docs/images/sidebar/sidebar-context-menu.png b/docs/images/sidebar/sidebar-context-menu.png
index 4fa08dd1..ce593714 100644
Binary files a/docs/images/sidebar/sidebar-context-menu.png and b/docs/images/sidebar/sidebar-context-menu.png differ
diff --git a/docs/images/sidebar/sidebar-cross-node-search.png b/docs/images/sidebar/sidebar-cross-node-search.png
index 287e071c..11200906 100644
Binary files a/docs/images/sidebar/sidebar-cross-node-search.png and b/docs/images/sidebar/sidebar-cross-node-search.png differ
diff --git a/docs/images/sidebar/sidebar-filter-chips.png b/docs/images/sidebar/sidebar-filter-chips.png
index c01cb396..8113ed6d 100644
Binary files a/docs/images/sidebar/sidebar-filter-chips.png and b/docs/images/sidebar/sidebar-filter-chips.png differ
diff --git a/docs/images/sidebar/sidebar-overview.png b/docs/images/sidebar/sidebar-overview.png
index 512cd76a..050e75ef 100644
Binary files a/docs/images/sidebar/sidebar-overview.png and b/docs/images/sidebar/sidebar-overview.png differ
diff --git a/docs/images/sidebar/sidebar-pinned.png b/docs/images/sidebar/sidebar-pinned.png
index 7cebd24a..19cb02cc 100644
Binary files a/docs/images/sidebar/sidebar-pinned.png and b/docs/images/sidebar/sidebar-pinned.png differ
diff --git a/docs/images/sidebar/sidebar-row-anatomy.png b/docs/images/sidebar/sidebar-row-anatomy.png
index 8cf3ed49..e763dca6 100644
Binary files a/docs/images/sidebar/sidebar-row-anatomy.png and b/docs/images/sidebar/sidebar-row-anatomy.png differ
diff --git a/docs/images/sso/sso-settings.png b/docs/images/sso/sso-settings.png
index d840f567..62a75342 100644
Binary files a/docs/images/sso/sso-settings.png and b/docs/images/sso/sso-settings.png differ
diff --git a/docs/images/stack-activity/activity-tab-empty.png b/docs/images/stack-activity/activity-tab-empty.png
index 3ad6f9bf..87579433 100644
Binary files a/docs/images/stack-activity/activity-tab-empty.png and b/docs/images/stack-activity/activity-tab-empty.png differ
diff --git a/docs/images/stack-activity/activity-tab-populated.png b/docs/images/stack-activity/activity-tab-populated.png
index a7975241..ed3274ad 100644
Binary files a/docs/images/stack-activity/activity-tab-populated.png and b/docs/images/stack-activity/activity-tab-populated.png differ
diff --git a/docs/images/stack-dossier/dossier-doc-drift.png b/docs/images/stack-dossier/dossier-doc-drift.png
index f4cad4a1..4ff950d9 100644
Binary files a/docs/images/stack-dossier/dossier-doc-drift.png and b/docs/images/stack-dossier/dossier-doc-drift.png differ
diff --git a/docs/images/stack-dossier/dossier-export-bar.png b/docs/images/stack-dossier/dossier-export-bar.png
index 63aecb65..31675065 100644
Binary files a/docs/images/stack-dossier/dossier-export-bar.png and b/docs/images/stack-dossier/dossier-export-bar.png differ
diff --git a/docs/images/stack-dossier/dossier-overview.png b/docs/images/stack-dossier/dossier-overview.png
index 877d64c6..010a41fe 100644
Binary files a/docs/images/stack-dossier/dossier-overview.png and b/docs/images/stack-dossier/dossier-overview.png differ
diff --git a/docs/images/stack-drift/drift-drifted.png b/docs/images/stack-drift/drift-drifted.png
index 204df728..6cef21a5 100644
Binary files a/docs/images/stack-drift/drift-drifted.png and b/docs/images/stack-drift/drift-drifted.png differ
diff --git a/docs/images/stack-drift/drift-history.png b/docs/images/stack-drift/drift-history.png
index 88352e86..d0a83d87 100644
Binary files a/docs/images/stack-drift/drift-history.png and b/docs/images/stack-drift/drift-history.png differ
diff --git a/docs/images/stack-drift/drift-in-sync.png b/docs/images/stack-drift/drift-in-sync.png
index b4237642..d5ff7588 100644
Binary files a/docs/images/stack-drift/drift-in-sync.png and b/docs/images/stack-drift/drift-in-sync.png differ
diff --git a/docs/images/stack-drift/drift-tab-location.png b/docs/images/stack-drift/drift-tab-location.png
index 24f89787..de7518c9 100644
Binary files a/docs/images/stack-drift/drift-tab-location.png and b/docs/images/stack-drift/drift-tab-location.png differ
diff --git a/docs/images/stack-file-explorer/context-menu-file.png b/docs/images/stack-file-explorer/context-menu-file.png
index e18a8ac9..5c3c29f2 100644
Binary files a/docs/images/stack-file-explorer/context-menu-file.png and b/docs/images/stack-file-explorer/context-menu-file.png differ
diff --git a/docs/images/stack-file-explorer/context-menu-folder.png b/docs/images/stack-file-explorer/context-menu-folder.png
index ecf27d23..29ad9ba0 100644
Binary files a/docs/images/stack-file-explorer/context-menu-folder.png and b/docs/images/stack-file-explorer/context-menu-folder.png differ
diff --git a/docs/images/stack-file-explorer/delete-protected-confirm.png b/docs/images/stack-file-explorer/delete-protected-confirm.png
index 0485ba91..e9033303 100644
Binary files a/docs/images/stack-file-explorer/delete-protected-confirm.png and b/docs/images/stack-file-explorer/delete-protected-confirm.png differ
diff --git a/docs/images/stack-file-explorer/full-screen-mode.png b/docs/images/stack-file-explorer/full-screen-mode.png
new file mode 100644
index 00000000..89469376
Binary files /dev/null and b/docs/images/stack-file-explorer/full-screen-mode.png differ
diff --git a/docs/images/stack-file-explorer/layout-panes.png b/docs/images/stack-file-explorer/layout-panes.png
deleted file mode 100644
index 90ae89fe..00000000
Binary files a/docs/images/stack-file-explorer/layout-panes.png and /dev/null differ
diff --git a/docs/images/stack-file-explorer/new-file-dialog.png b/docs/images/stack-file-explorer/new-file-dialog.png
index 18a5b8da..7eaa9c4c 100644
Binary files a/docs/images/stack-file-explorer/new-file-dialog.png and b/docs/images/stack-file-explorer/new-file-dialog.png differ
diff --git a/docs/images/stack-file-explorer/overview.png b/docs/images/stack-file-explorer/overview.png
index 4a787a43..fe2c59de 100644
Binary files a/docs/images/stack-file-explorer/overview.png and b/docs/images/stack-file-explorer/overview.png differ
diff --git a/docs/images/stack-file-explorer/permissions-dialog.png b/docs/images/stack-file-explorer/permissions-dialog.png
index 0983fecd..f2c4e871 100644
Binary files a/docs/images/stack-file-explorer/permissions-dialog.png and b/docs/images/stack-file-explorer/permissions-dialog.png differ
diff --git a/docs/images/stack-file-explorer/protected-tree-marker.png b/docs/images/stack-file-explorer/protected-tree-marker.png
index 5484b204..7fcd26ef 100644
Binary files a/docs/images/stack-file-explorer/protected-tree-marker.png and b/docs/images/stack-file-explorer/protected-tree-marker.png differ
diff --git a/docs/images/stack-file-explorer/viewer-edit-mode.png b/docs/images/stack-file-explorer/viewer-edit-mode.png
index abf2d18b..ad05b24a 100644
Binary files a/docs/images/stack-file-explorer/viewer-edit-mode.png and b/docs/images/stack-file-explorer/viewer-edit-mode.png differ
diff --git a/docs/images/stack-labels/context-menu-labels.png b/docs/images/stack-labels/context-menu-labels.png
index 1b604dbb..eec7799e 100644
Binary files a/docs/images/stack-labels/context-menu-labels.png and b/docs/images/stack-labels/context-menu-labels.png differ
diff --git a/docs/images/stack-labels/create-label-dialog.png b/docs/images/stack-labels/create-label-dialog.png
index beaab3ee..e0d7af84 100644
Binary files a/docs/images/stack-labels/create-label-dialog.png and b/docs/images/stack-labels/create-label-dialog.png differ
diff --git a/docs/images/stack-labels/fleet-actions.png b/docs/images/stack-labels/fleet-actions.png
index 14dcfc59..d2fde4a4 100644
Binary files a/docs/images/stack-labels/fleet-actions.png and b/docs/images/stack-labels/fleet-actions.png differ
diff --git a/docs/images/stack-labels/fleet-stop-preview.png b/docs/images/stack-labels/fleet-stop-preview.png
index 9bda87fe..9e8abfc3 100644
Binary files a/docs/images/stack-labels/fleet-stop-preview.png and b/docs/images/stack-labels/fleet-stop-preview.png differ
diff --git a/docs/images/stack-labels/fleet-tags-filter.png b/docs/images/stack-labels/fleet-tags-filter.png
index 74b0e625..6a70bd84 100644
Binary files a/docs/images/stack-labels/fleet-tags-filter.png and b/docs/images/stack-labels/fleet-tags-filter.png differ
diff --git a/docs/images/stack-labels/inline-create-form.png b/docs/images/stack-labels/inline-create-form.png
index 42b413e0..322947f0 100644
Binary files a/docs/images/stack-labels/inline-create-form.png and b/docs/images/stack-labels/inline-create-form.png differ
diff --git a/docs/images/stack-labels/settings-labels.png b/docs/images/stack-labels/settings-labels.png
index 902fbbf2..787778ff 100644
Binary files a/docs/images/stack-labels/settings-labels.png and b/docs/images/stack-labels/settings-labels.png differ
diff --git a/docs/images/stack-labels/sidebar-grouping.png b/docs/images/stack-labels/sidebar-grouping.png
index 2bc55a73..0121c125 100644
Binary files a/docs/images/stack-labels/sidebar-grouping.png and b/docs/images/stack-labels/sidebar-grouping.png differ
diff --git a/docs/images/stack-management/bulk-action-bar.png b/docs/images/stack-management/bulk-action-bar.png
index a0e9f288..6762f3fc 100644
Binary files a/docs/images/stack-management/bulk-action-bar.png and b/docs/images/stack-management/bulk-action-bar.png differ
diff --git a/docs/images/stack-management/context-menu.png b/docs/images/stack-management/context-menu.png
index e2a6e56d..f0358fe0 100644
Binary files a/docs/images/stack-management/context-menu.png and b/docs/images/stack-management/context-menu.png differ
diff --git a/docs/images/stack-management/convert-tab-empty.png b/docs/images/stack-management/convert-tab-empty.png
index 7576fb94..49e9a227 100644
Binary files a/docs/images/stack-management/convert-tab-empty.png and b/docs/images/stack-management/convert-tab-empty.png differ
diff --git a/docs/images/stack-management/convert-tab-error.png b/docs/images/stack-management/convert-tab-error.png
index 0436227c..6ebdfd1a 100644
Binary files a/docs/images/stack-management/convert-tab-error.png and b/docs/images/stack-management/convert-tab-error.png differ
diff --git a/docs/images/stack-management/convert-tab-result.png b/docs/images/stack-management/convert-tab-result.png
index d841bead..582f6134 100644
Binary files a/docs/images/stack-management/convert-tab-result.png and b/docs/images/stack-management/convert-tab-result.png differ
diff --git a/docs/images/stack-management/create-stack-dialog.png b/docs/images/stack-management/create-stack-dialog.png
index 6d1af20f..be380a51 100644
Binary files a/docs/images/stack-management/create-stack-dialog.png and b/docs/images/stack-management/create-stack-dialog.png differ
diff --git a/docs/images/stack-management/create-stack-git.png b/docs/images/stack-management/create-stack-git.png
index 0028aae7..2af02d17 100644
Binary files a/docs/images/stack-management/create-stack-git.png and b/docs/images/stack-management/create-stack-git.png differ
diff --git a/docs/images/stack-management/filter-chips.png b/docs/images/stack-management/filter-chips.png
index d84009ff..a6fe6007 100644
Binary files a/docs/images/stack-management/filter-chips.png and b/docs/images/stack-management/filter-chips.png differ
diff --git a/docs/images/stack-management/sidebar-actions.png b/docs/images/stack-management/sidebar-actions.png
index 305816e2..64e96a2a 100644
Binary files a/docs/images/stack-management/sidebar-actions.png and b/docs/images/stack-management/sidebar-actions.png differ
diff --git a/docs/images/stack-management/sidebar-grouped.png b/docs/images/stack-management/sidebar-grouped.png
index f1e14524..41765aca 100644
Binary files a/docs/images/stack-management/sidebar-grouped.png and b/docs/images/stack-management/sidebar-grouped.png differ
diff --git a/docs/images/stack-view/anatomy-panel.png b/docs/images/stack-view/anatomy-panel.png
index 1977af74..af5c895d 100644
Binary files a/docs/images/stack-view/anatomy-panel.png and b/docs/images/stack-view/anatomy-panel.png differ
diff --git a/docs/images/stack-view/containers.png b/docs/images/stack-view/containers.png
index 962c9287..6e1a31cc 100644
Binary files a/docs/images/stack-view/containers.png and b/docs/images/stack-view/containers.png differ
diff --git a/docs/images/stack-view/identity-header.png b/docs/images/stack-view/identity-header.png
index 1936aa97..23814517 100644
Binary files a/docs/images/stack-view/identity-header.png and b/docs/images/stack-view/identity-header.png differ
diff --git a/docs/images/stack-view/logs-viewer.png b/docs/images/stack-view/logs-viewer.png
index 7e8fe159..bf7f7482 100644
Binary files a/docs/images/stack-view/logs-viewer.png and b/docs/images/stack-view/logs-viewer.png differ
diff --git a/docs/images/stacks.png b/docs/images/stacks.png
index 9ad8876a..7a2f4397 100644
Binary files a/docs/images/stacks.png and b/docs/images/stacks.png differ
diff --git a/docs/images/two-factor-auth/account-card-disabled.png b/docs/images/two-factor-auth/account-card-disabled.png
index aa3f4c16..e96d0e54 100644
Binary files a/docs/images/two-factor-auth/account-card-disabled.png and b/docs/images/two-factor-auth/account-card-disabled.png differ
diff --git a/docs/images/two-factor-auth/account-card-enabled.png b/docs/images/two-factor-auth/account-card-enabled.png
index 2ef1aab4..204bd675 100644
Binary files a/docs/images/two-factor-auth/account-card-enabled.png and b/docs/images/two-factor-auth/account-card-enabled.png differ
diff --git a/docs/images/two-factor-auth/admin-reset.png b/docs/images/two-factor-auth/admin-reset.png
index a1494b31..7493023a 100644
Binary files a/docs/images/two-factor-auth/admin-reset.png and b/docs/images/two-factor-auth/admin-reset.png differ
diff --git a/docs/images/two-factor-auth/challenge-backup.png b/docs/images/two-factor-auth/challenge-backup.png
index cfb1ea75..fba14804 100644
Binary files a/docs/images/two-factor-auth/challenge-backup.png and b/docs/images/two-factor-auth/challenge-backup.png differ
diff --git a/docs/images/two-factor-auth/challenge.png b/docs/images/two-factor-auth/challenge.png
index 7005ec5b..1a0f7a9d 100644
Binary files a/docs/images/two-factor-auth/challenge.png and b/docs/images/two-factor-auth/challenge.png differ
diff --git a/docs/images/two-factor-auth/disable-confirm.png b/docs/images/two-factor-auth/disable-confirm.png
index bba546a9..21331f24 100644
Binary files a/docs/images/two-factor-auth/disable-confirm.png and b/docs/images/two-factor-auth/disable-confirm.png differ
diff --git a/docs/images/two-factor-auth/enroll-backup-codes.png b/docs/images/two-factor-auth/enroll-backup-codes.png
index ec2cc084..6528eebd 100644
Binary files a/docs/images/two-factor-auth/enroll-backup-codes.png and b/docs/images/two-factor-auth/enroll-backup-codes.png differ
diff --git a/docs/images/two-factor-auth/enroll-confirm.png b/docs/images/two-factor-auth/enroll-confirm.png
index cd3c84b8..000756a9 100644
Binary files a/docs/images/two-factor-auth/enroll-confirm.png and b/docs/images/two-factor-auth/enroll-confirm.png differ
diff --git a/docs/images/two-factor-auth/enroll-qr.png b/docs/images/two-factor-auth/enroll-qr.png
index acb4abe8..56d3286e 100644
Binary files a/docs/images/two-factor-auth/enroll-qr.png and b/docs/images/two-factor-auth/enroll-qr.png differ
diff --git a/docs/images/two-factor-auth/regenerate-confirm.png b/docs/images/two-factor-auth/regenerate-confirm.png
index f7db5ff8..bc35d0af 100644
Binary files a/docs/images/two-factor-auth/regenerate-confirm.png and b/docs/images/two-factor-auth/regenerate-confirm.png differ
diff --git a/docs/images/two-factor-auth/regenerate-show.png b/docs/images/two-factor-auth/regenerate-show.png
index 3354eab4..5ec68a4f 100644
Binary files a/docs/images/two-factor-auth/regenerate-show.png and b/docs/images/two-factor-auth/regenerate-show.png differ
diff --git a/docs/images/vulnerability-scanning/add-policy.png b/docs/images/vulnerability-scanning/add-policy.png
new file mode 100644
index 00000000..c51c02e5
Binary files /dev/null and b/docs/images/vulnerability-scanning/add-policy.png differ
diff --git a/docs/images/vulnerability-scanning/app-store-toggle.png b/docs/images/vulnerability-scanning/app-store-toggle.png
index eac15115..808071e8 100644
Binary files a/docs/images/vulnerability-scanning/app-store-toggle.png and b/docs/images/vulnerability-scanning/app-store-toggle.png differ
diff --git a/docs/images/vulnerability-scanning/resources-badges.png b/docs/images/vulnerability-scanning/resources-badges.png
index 74c7b13d..ccf01832 100644
Binary files a/docs/images/vulnerability-scanning/resources-badges.png and b/docs/images/vulnerability-scanning/resources-badges.png differ
diff --git a/docs/images/vulnerability-scanning/scan-compare-sheet.png b/docs/images/vulnerability-scanning/scan-compare-sheet.png
index 923f835a..f3251585 100644
Binary files a/docs/images/vulnerability-scanning/scan-compare-sheet.png and b/docs/images/vulnerability-scanning/scan-compare-sheet.png differ
diff --git a/docs/images/vulnerability-scanning/scan-config-button.png b/docs/images/vulnerability-scanning/scan-config-button.png
index d5580280..bf0fb67f 100644
Binary files a/docs/images/vulnerability-scanning/scan-config-button.png and b/docs/images/vulnerability-scanning/scan-config-button.png differ
diff --git a/docs/images/vulnerability-scanning/scan-details-sheet.png b/docs/images/vulnerability-scanning/scan-details-sheet.png
index 8f0bd0ad..964da8c9 100644
Binary files a/docs/images/vulnerability-scanning/scan-details-sheet.png and b/docs/images/vulnerability-scanning/scan-details-sheet.png differ
diff --git a/docs/images/vulnerability-scanning/scan-history-sheet.png b/docs/images/vulnerability-scanning/scan-history-sheet.png
index db956016..b4b22aa3 100644
Binary files a/docs/images/vulnerability-scanning/scan-history-sheet.png and b/docs/images/vulnerability-scanning/scan-history-sheet.png differ
diff --git a/docs/images/vulnerability-scanning/secrets-tab.png b/docs/images/vulnerability-scanning/secrets-tab.png
new file mode 100644
index 00000000..33ff36e1
Binary files /dev/null and b/docs/images/vulnerability-scanning/secrets-tab.png differ
diff --git a/docs/images/vulnerability-scanning/security-settings.png b/docs/images/vulnerability-scanning/security-settings.png
deleted file mode 100644
index ed1e2852..00000000
Binary files a/docs/images/vulnerability-scanning/security-settings.png and /dev/null differ
diff --git a/docs/images/vulnerability-scanning/trivy-settings-card.png b/docs/images/vulnerability-scanning/trivy-settings-card.png
index b1613d3d..e4cb5ac7 100644
Binary files a/docs/images/vulnerability-scanning/trivy-settings-card.png and b/docs/images/vulnerability-scanning/trivy-settings-card.png differ
diff --git a/docs/images/webhooks/webhooks-create-form.png b/docs/images/webhooks/webhooks-create-form.png
index 247fed1e..42a6f2e0 100644
Binary files a/docs/images/webhooks/webhooks-create-form.png and b/docs/images/webhooks/webhooks-create-form.png differ
diff --git a/docs/images/webhooks/webhooks-secret-reveal.png b/docs/images/webhooks/webhooks-secret-reveal.png
index a9ecd7a8..5ad3124e 100644
Binary files a/docs/images/webhooks/webhooks-secret-reveal.png and b/docs/images/webhooks/webhooks-secret-reveal.png differ
diff --git a/docs/images/webhooks/webhooks-settings.png b/docs/images/webhooks/webhooks-settings.png
index 5897caf0..a9bbc4ae 100644
Binary files a/docs/images/webhooks/webhooks-settings.png and b/docs/images/webhooks/webhooks-settings.png differ
diff --git a/docs/operations/backup.mdx b/docs/operations/backup.mdx
index 6ea38122..f246dcf4 100644
--- a/docs/operations/backup.mdx
+++ b/docs/operations/backup.mdx
@@ -19,10 +19,10 @@ Default path: `/app/data` inside the container, mapped to wherever you mounted i
Contains:
- `sencho.db` - SQLite database with all settings, users, nodes, alerts, metrics history, and notification config
-- `encryption.key` - Key used to encrypt sensitive values (registry credentials, node API tokens) stored in the database
+- `encryption.key` - Key used to encrypt sensitive values stored in the database: registry credentials, node tokens for remote nodes, Git source credentials, [Fleet Secrets](/features/fleet-secrets) bundles, SSO and MFA secrets, Recovery Vault credentials, and the contents of every [Fleet Snapshot](/features/fleet-backups)
+
Behind the scenes:
@@ -49,7 +53,7 @@ The install, update, and uninstall buttons and the **Auto-update Trivy** toggle
### Removing the managed install
-Click **Uninstall** next to the status badge. Sencho removes the binary from `/app/data/bin/trivy`. The vulnerability database cache at `/app/data/trivy-cache` is left in place in case you reinstall later; delete it manually if you want to reclaim the disk space.
+Click **Uninstall** next to the status badge and confirm in the dialog. Sencho removes the binary from `/app/data/bin/trivy`; vulnerability scanning stops working until Trivy is reinstalled or a host binary is provided. The vulnerability database cache at `/app/data/trivy-cache` is left in place in case you reinstall later; delete it manually if you want to reclaim the disk space.
## Option 2: Installing Trivy on the host
@@ -57,8 +61,8 @@ Click **Uninstall** next to the status badge. Sencho removes the binary from `/a
```bash
sudo apt-get install wget gnupg
-wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
-echo "deb https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list
+wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
+echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy
```
@@ -66,12 +70,12 @@ sudo apt-get install trivy
### Linux (RHEL / Fedora)
```bash
-sudo rpm --import https://aquasecurity.github.io/trivy-repo/rpm/public.key
echo "[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/\$basearch/
gpgcheck=1
-enabled=1" | sudo tee /etc/yum.repos.d/trivy.repo
+enabled=1
+gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key" | sudo tee /etc/yum.repos.d/trivy.repo
sudo dnf install trivy
```
@@ -91,7 +95,7 @@ The command should print a `Version: X.Y.Z` line. Note the path that `which triv
## Making a host-installed Trivy available to Sencho
-If you already installed Trivy on the host (Option 2) and prefer to manage it externally, Sencho runs inside a container and looks for `trivy` on its own `PATH`. There are two ways to expose it:
+If you already installed Trivy on the host (Option 2) and prefer to manage it externally, Sencho looks for a binary in this order: the managed install, then the path in `TRIVY_BIN` if set, then `trivy` on the container's own `PATH`. There are two ways to expose a host binary:
### Bind mount the host binary
@@ -119,7 +123,20 @@ The `trivy-cache` volume persists the vulnerability database across Sencho conta
Adjust the first path if `which trivy` on the host prints something other than `/usr/local/bin/trivy` (for example `/usr/bin/trivy` on some distributions).
-### Option 3: Build a custom Sencho image
+### Mounting to a non-PATH location
+
+If you would rather not put the binary on `PATH` inside the container, mount it anywhere and point `TRIVY_BIN` at it instead:
+
+```yaml
+services:
+ sencho:
+ volumes:
+ - /opt/trivy/trivy:/opt/trivy/trivy:ro
+ environment:
+ - TRIVY_BIN=/opt/trivy/trivy
+```
+
+## Option 3: Build a custom Sencho image
If the host's Trivy binary is not ABI-compatible with the Sencho container (for example because you are running macOS host binaries or a different glibc version), install Trivy inside the image instead:
@@ -180,10 +197,16 @@ Trivy supports offline use through pre-built database bundles.
Plan to refresh the bundle on a schedule (weekly is typical) so CVE data stays current.
+The Scanner setup tab also has an **Exploit intelligence (KEV + EPSS)** toggle, on by default, that independently reaches `cisa.gov` and `api.first.org` once a day to prioritize findings. It is unrelated to the Trivy database bundle above, so turn it off separately if the host must not make any outbound calls at all.
+
## Verifying Sencho detects Trivy
1. Open the **Security** page → **Scanner setup** tab. The **Vulnerability Scanner** card shows the current status and version.
-2. Open the **Resources** tab. If Trivy is detected, a shield icon appears in the Actions column of the **Images** panel next to the delete icon on every row.
+2. Open the **Resources** page → **Images** tab. If Trivy is detected, a shield icon appears in the Action column next to the delete icon on every row with a tag.
+
+
+
+
If the scanner shows as not installed after using Option 2 or 3, see the troubleshooting section below.
@@ -203,7 +226,7 @@ Verify the binary is visible from inside the container:
docker exec sencho trivy --version
```
-If the command returns "not found", the mount path inside the container is wrong. The binary must be on `PATH`. Both `/usr/local/bin/trivy` and `/usr/bin/trivy` work.
+If the command returns "not found", the mount path inside the container is wrong. The binary must either be on `PATH` (`/usr/local/bin/trivy` and `/usr/bin/trivy` both work) or match the path set in `TRIVY_BIN`.
### Binary exists but reports an exec format error
@@ -215,7 +238,7 @@ The first scan after a Trivy install downloads the vulnerability database. Expec
### Install button is hidden
-The install button is hidden when a host-installed Trivy is already detected on `PATH`. Remove the host binary (or drop the bind mount) to switch to the managed install. The button also requires the admin role; viewer accounts see the scanner status only.
+The install button is hidden when a host-installed Trivy is already detected, whether via `TRIVY_BIN` or on `PATH`. Remove the host binary (or drop the bind mount and unset `TRIVY_BIN`) to switch to the managed install. The button also requires the admin role; viewer accounts see the scanner status only.
### Private registry images fail to scan
diff --git a/docs/operations/troubleshooting.mdx b/docs/operations/troubleshooting.mdx
index 0bd7028b..41d8af3b 100644
--- a/docs/operations/troubleshooting.mdx
+++ b/docs/operations/troubleshooting.mdx
@@ -168,14 +168,16 @@ Traefik handles WebSocket upgrades automatically for HTTP/1.1 backends, so no ex
**Symptom:** A node you added shows a red or gray status dot.
-**Checks in order:**
+Sencho connects to a remote node one of two ways: **Distributed API Proxy** mode (the primary reaches out to the remote's API URL) or **Pilot Agent** mode (the remote dials an outbound tunnel to the primary, so no inbound port is needed on the remote). The checks below cover Proxy mode; if the node is a Pilot Agent, see [Pilot Agent troubleshooting](/features/pilot-agent#troubleshooting) for the tunnel-specific causes (expired enrollment token, agent container not running, blocked outbound HTTPS).
+
+**Checks in order (Distributed API Proxy mode):**
1. **Is the remote Sencho instance running?** SSH to that machine and verify.
2. **Is the API URL correct?** It must include the protocol and port (e.g. `http://192.168.1.20:3001`). Open it in a browser; you should see a JSON response from `/api/health`.
-3. **Is the token correct?** Tokens are long JWT strings. Even one missing character will cause auth to fail. Regenerate the token on the remote instance and update the node config.
+3. **Is the token correct?** Tokens are long JWT strings. Even one missing character will cause auth to fail. Regenerating the token on the remote instance invalidates the previous one, so update the saved node config with the new value.
4. **Is there a firewall blocking the port?** The primary Sencho host must be able to reach the remote host's Sencho port.
-To re-test connectivity after making changes, open **Profile > Settings > Nodes** and click the **wifi icon** on the node row.
+To re-test connectivity after making changes, open **Settings · Infrastructure · Nodes** and click **Test Connection** on the node row. See the [Multi-Node Management troubleshooting section](/features/multi-node#troubleshooting) for additional connectivity scenarios.
---
@@ -232,8 +234,7 @@ To re-test connectivity after making changes, open **Profile > Settings > Nodes*
1. **Is your primary instance licensed?** Open **Settings → Admiral Account** on the primary instance and verify it shows an active Admiral license. Remote nodes inherit the primary's tier; if the primary is on Community, all remote nodes will be Community too.
2. **Is the remote node's token valid?** An expired or revoked token prevents the license tier from being transmitted. Regenerate the token on the remote instance and update the node config on the primary.
-3. **Is the remote node running an up-to-date version of Sencho?** Distributed license enforcement requires both the primary and remote instances to be on a compatible version. Update the remote node if it's outdated.
-4. **Are you accessing the remote node directly?** If you navigate directly to the remote Sencho instance's URL (bypassing the primary), it uses its own local license. License inheritance only works through the primary's proxy.
+3. **Are you accessing the remote node directly?** If you navigate directly to the remote Sencho instance's URL (bypassing the primary), it uses its own local license. License inheritance only works through the primary's proxy.
---
@@ -263,12 +264,9 @@ To re-test connectivity after making changes, open **Profile > Settings > Nodes*
**Symptom:** A stack appears in the sidebar with no status indicator (shown as "--"), but clicking on it reveals running containers with active stats.
-**Cause:** The sidebar status check matches containers by their Docker Compose project label. A mismatch can happen when:
+**Cause:** The sidebar status check matches containers by their Docker Compose project label. A mismatch can happen when the compose file declares a top-level `name:` field that overrides the default project name.
-- The compose file declares a top-level `name:` field that overrides the default project name.
-- Containers were created before Sencho reorganized compose files into subdirectories, so their labels still reference the old layout.
-
-**Fix:** Update to the latest version of Sencho. The status check uses multiple fallback strategies (project name mapping, service name, and config file paths) to match containers regardless of how they were originally started.
+**Fix:** The status check uses multiple fallback strategies (project name mapping, service name, and config file paths) to match containers regardless of how they were originally started, so this usually resolves itself on the next status refresh. If it persists, confirm the containers are actually part of this stack's compose project with `docker compose -f /path/to/your/stack/compose.yaml ps`.
---
@@ -280,7 +278,7 @@ To re-test connectivity after making changes, open **Profile > Settings > Nodes*
- Names must be non-empty and cannot contain only whitespace.
- Allowed characters: letters, numbers, hyphens (`-`), underscores (`_`), and dots (`.`).
-- Names must start with a letter, number, or underscore. They cannot start with a dot or hyphen.
+- Names must start with a letter or number. They cannot start with a dot, hyphen, or underscore.
| Name | Valid? | Reason |
|------|--------|--------|
@@ -288,6 +286,7 @@ To re-test connectivity after making changes, open **Profile > Settings > Nodes*
| `app_net.v2` | Yes | |
| `.hidden` | No | Cannot start with a dot |
| `-leading` | No | Cannot start with a hyphen |
+| `_leading` | No | Cannot start with an underscore |
| `my network` | No | Spaces not allowed |
| `net/work` | No | Slashes not allowed |
@@ -366,7 +365,7 @@ volumes:
- /var/run/docker.sock:/var/run/docker.sock
```
-Nodes running very old Sencho versions that predate the capability system need to be updated manually first. After that, future updates can be triggered from the dashboard.
+After adding the mount and redeploying the remote, it advertises the capability and future updates can be triggered from the dashboard.
---
@@ -406,7 +405,7 @@ docker compose pull && docker compose up -d
1. **Immediate failure:** The gateway could not reach the remote node's update endpoint, or the remote node rejected the request. Common reasons include network issues, expired API tokens, or the remote node not supporting self-update.
-2. **Delayed failure (after ~90 seconds):** The update was accepted by the remote node, but the node never restarted. This usually means the image pull failed on the remote host. The error message will read: *"Update may have failed. The node is still running and its version has not changed."*
+2. **Delayed failure (after ~3 minutes):** The update was accepted by the remote node, but the node never restarted. This usually means the image pull failed on the remote host. The error message will read: *"Update may have failed. The node is still running and its version has not changed."*
**Fix:**
@@ -456,35 +455,18 @@ After adding the missing volume mount, restart Sencho and retry the update.
---
-## First remote update always times out on old nodes
-
-**Symptom:** After triggering a remote update on a node running a very old Sencho version (pre-v0.39.3), the node successfully restarts with the new version, but the dashboard shows **Timed out** or **Failed** instead of **Updated**.
-
-**Cause:** The gateway detects update completion using three signals: a version change, a process restart (via `startedAt`), or temporary offline detection. Nodes running Sencho versions older than v0.39.3 report their version as `"unknown"` and do not include `startedAt` in their metadata. If the container restart is brief enough that the gateway does not detect the node going offline, none of the three signals fire.
-
-**Fix:** This only affects the **first** update from a very old version. After the update completes (even though the dashboard may show "Failed" or "Timed out"), the remote node will be running the latest version with proper version reporting and restart detection. Future updates triggered from the dashboard will complete normally.
-
-1. Click **Dismiss** on the badge to clear the state.
-2. Click **Recheck** to verify the node is now running the latest version.
-3. If the node still shows "unknown", update it manually on the remote host:
-
-```bash
-docker compose pull && docker compose up -d
-```
-
----
-
## Remote node version shows "unknown"
**Symptom:** A remote node appears online in the Fleet View but its version badge is missing or the Node Updates modal shows "unknown" in the current version column.
**Common causes:**
-1. **Network issue:** The gateway cannot reach the remote node's `/api/meta` endpoint. Check that the API URL configured for the node is correct and accessible from the gateway host.
-2. **Old Sencho version:** Nodes running Sencho versions older than v0.39.0 do not report a version in their metadata. Update the remote node manually to resolve this.
-3. **Development build:** If running from source, ensure you have run `npm install` in the backend directory so the `predev` hook regenerates the version file.
+1. **Network issue:** The gateway cannot reach the remote node's `/api/meta` endpoint within its 5-second timeout. Check that the API URL configured for the node is correct and accessible from the gateway host.
+2. **Development build:** If running from source, ensure you have run `npm install` in the backend directory so the `predev` hook regenerates the version file.
-**To verify:** Open your browser's developer tools and check the response from `GET /api/fleet/update-status`. Each node's `version` field should contain a semver string (e.g., `"0.39.4"`). If it shows `null`, the gateway could not determine the remote's version.
+If a triggered update never resolves once the underlying cause above is fixed, click **Dismiss** on the badge to clear the stuck state, then **Recheck** to re-scan the fleet.
+
+**To verify:** Open your browser's developer tools and check the response from `GET /api/fleet/update-status`. Each node's `version` field should contain a semver string (e.g., `"0.96.0"`). If it shows `null`, the gateway could not determine the remote's version.
---
@@ -494,7 +476,7 @@ docker compose pull && docker compose up -d
**Checks:**
-1. **At least one label must exist**: Labels are managed in **Profile > Settings > Labels**. Create at least one label before assigning it to stacks.
+1. **At least one label must exist**: Labels are managed in **Settings · Organization · Labels**. Create at least one label before assigning it to stacks.
2. **Assignments are saved**: Open the stack's context menu, choose **Labels**, and confirm the label is checked. Pills appear in the sidebar once at least one label is assigned.
---
@@ -506,7 +488,7 @@ docker compose pull && docker compose up -d
**Checks:**
1. **Stack name match**: The stack name in the routing rule must match exactly. Stack names are case-sensitive and correspond to the directory name in your compose folder.
-2. **Rule is enabled**: Check that the route's toggle is turned on in **Profile > Settings > Notifications > Notification Routing**.
+2. **Rule is enabled**: Check that the route's toggle is turned on in **Settings · Notifications · Notification Routing**.
---
@@ -522,7 +504,7 @@ docker compose pull && docker compose up -d
**Symptom:** LDAP connection test fails with a TLS/certificate error.
-**Fix:** If your LDAP server uses a self-signed certificate, toggle off "Verify TLS certificate" in **Profile > Settings > SSO**. For production, install a trusted certificate instead.
+**Fix:** If your LDAP server uses a self-signed certificate, toggle off "Verify TLS certificate" in **Settings · SSO**. For production, install a trusted certificate instead.
---
@@ -544,7 +526,7 @@ docker compose pull && docker compose up -d
**Cause:** SSO providers only appear on the login page when they are both **configured** (saved with valid credentials) and **enabled** (toggle is on).
-**Fix:** Open **Profile > Settings > SSO** and verify the provider has an **Active** badge and its toggle is enabled.
+**Fix:** Open **Settings · SSO** and verify the provider has an **Active** badge and its toggle is enabled.
---
@@ -607,13 +589,6 @@ The backend logs all route errors and service failures to stdout. This is the fi
**Symptom:** The **From Docker Run** tab in the Create Stack dialog returns an error toast saying the command could not be parsed.
-**Cause:** The input is not a well-formed `docker run` command, or it uses a flag the converter does not recognize.
+**Cause:** The input is not a well-formed `docker run` command, uses a flag the converter does not recognize, or exceeds the length limit.
-**Fix:**
-
-- Make sure the command starts with `docker run` and ends with an image reference (e.g. `nginx:alpine`).
-- Check the [list of supported flags](/features/stack-management#supported-flags). Rare flags (such as `--userns` or custom runtime options) are not recognized. Remove the flag before converting and add it back manually in the editor afterwards.
-- Collapse multi-line commands into a single line. Mixed quoting across `\`-continued lines is a common source of parse errors.
-- Commands longer than 8192 characters are rejected. Trim anything non-essential and run the converter on the reduced command.
-
-When only part of a command is supported, convert what you can, paste the resulting YAML into the **Empty** tab as a starting point, and hand-edit the rest in `compose.yaml`.
+See [Convert from a docker run command](/features/stack-management#convert-from-a-docker-run-command) for the full list of supported flags and every rejection cause, including the exact size limit and the fix for quoting issues in multi-line commands.
diff --git a/docs/operations/two-factor-admin.mdx b/docs/operations/two-factor-admin.mdx
index 6dad9853..096179bc 100644
--- a/docs/operations/two-factor-admin.mdx
+++ b/docs/operations/two-factor-admin.mdx
@@ -32,6 +32,16 @@ After confirming:
Tell the user to re-enrol from **Settings · Account · Two-factor authentication · Set up 2FA** as soon as they are back in. The account is unprotected until they do.
+
@@ -72,10 +74,10 @@ The Account section manages password and two-factor authentication for the opera
| Field | Description |
|-------|-------------|
| **Current password** | Your existing password. Required to change any auth setting. |
-| **New password** | Minimum 8 characters. 12 or more is recommended. |
+| **New password** | 8 characters minimum, enforced. The strength indicator recommends 12+ characters with mixed case and a number, but this is guidance only. |
| **Confirm new password** | Must match New password. |
-Click **Update password** to apply. Existing sessions remain valid.
+Click **Update password** to apply. Your current session stays signed in; every other session for this account is invalidated immediately.
### Two-factor authentication
@@ -124,34 +126,49 @@ Control how Sencho looks and how dense the workspace feels. Each browser remembe
### Theme and typography
-The **Mode** (Dim, OLED, Light, Auto) and **Accent** (one of eight hues) set the surface palette and data color; **Border brightness** tunes every hairline. The **Interface font**, **Data font**, and **Text size** are yours to change; the heading style follows your Visual style choice.
+A live preview card shows a sample fleet-status tile so you can see a color choice before committing to it. The **Mode** (Dim, OLED, Light, Auto) and **Accent** (one of eight hues) set the surface palette and data color; **Border brightness** tunes every hairline.
-### Density
+| Control | Options |
+|---------|---------|
+| **Interface font** | Geist, IBM Plex, or Hanken. The sans face for body text, labels, navigation, and buttons. Heading style follows your Visual style choice. |
+| **Data font** | Geist Mono, Plex Mono, or Fira Code. The monospace face for terminal output, stats, codes, and timestamps. |
+| **Text size** | Scales the whole interface from a single root multiplier. Default 1.00x. |
-| Value | When to pick it |
-|-------|-----------------|
-| **Comfortable** | Default spacing. Roomier rows and tiles for review and orientation. |
-| **Compact** | Tighter rows and tiles. Fits more stacks, tasks, and audit entries on screen at once. |
+### Display
-Density affects the dashboard stack table, the resource gauge strip, the Settings Hub sidebar, the Schedules and Audit Log tables, and every other data table in Sencho. Typography, color, and layout structure stay the same; only vertical padding compresses.
+| Control | What it does |
+|---------|--------------|
+| **Density** | **Comfortable** (default spacing, roomier rows and tiles for review and orientation) or **Compact** (tighter rows and tiles, fits more stacks, tasks, and audit entries on screen at once). Affects the dashboard stack table, the resource gauge strip, the Settings Hub sidebar, the Schedules and Audit Log tables, and every other data table in Sencho. Typography, color, and layout structure stay the same; only vertical padding compresses. |
+| **Log chip color** | **Unified** uses the accent color for every service's log chip. **Per service** assigns each service a stable label color for faster visual scanning across a busy log stream. |
+
+### Navigation
+
+| Control | What it does |
+|---------|--------------|
+| **Navigation style** | **Smart bar** (recommended default): primary destinations stay visible in the top bar and the rest live under **More**. **Classic bar** keeps the full horizontal strip of destinations. **Compact launcher** puts every destination in a menu, with optional quick links. |
+| **Top navigation labels** | On by default. Shows text labels beside the top navigation icons; turn off for a more compact bar with icons only. |
Deploy-progress behavior and the diff-preview-before-save step are stack workflow preferences and live in their own [Stacks](#stacks) section under Infrastructure.
---
-## License
+## Admiral Account
**Scope:** Operator-facing, but reflects the instance license
-Activate, view, or deactivate the license for this Sencho control plane. The masthead publishes a **PLAN** pill showing the current tier, a **TRIAL** pill with days remaining if a trial is active, a **RENEWS** pill with the next renewal date, and a **STATUS** pill if the license is in an unusual state (expired, error).
+Activate, view, or deactivate the license for this Sencho control plane, and see which image channel it is tracking. The masthead publishes a **PLAN** pill showing the current tier, a **DURATION** pill (for example `lifetime` or a renewal cadence), a **TRIAL** pill with days remaining if a trial is active, a **RENEWS** pill with the next renewal date, and a **STATUS** pill if the license is in an unusual state (expired, error).
-
+
| Element | Description |
|---------|-------------|
-| **Plan** | The active tier (Sencho Community, Sencho Admiral, or Sencho Admiral (Trial)) with a tier badge. |
+| **Sencho Admiral** | The active license on this control plane, with a tier badge. Community instances see an upgrade prompt here instead. |
+| **Recovery Vault** | Whether the current subscription includes Recovery Vault entitlement. |
+| **Hardened Build** | Switches this control plane between the Community image channel and the Admiral Hardened Build channel. Review entitlement and registry access before switching; see [Plans](/features/licensing#feature-breakdown) for what Hardened Build changes. |
+| **Current image** | The image reference this control plane is currently running, so you can confirm which channel took effect after a switch. |
+| **Channel** | The active image channel (Community or Hardened). |
| **Customer** | The customer name on file with Lemon Squeezy (paid plans only). |
| **Product** | The product (paid plans only). |
| **License key** | The active key, masked to the last four characters. |
@@ -160,6 +177,8 @@ Activate, view, or deactivate the license for this Sencho control plane. The mas
| **Deactivate** | Releases the key from this instance and reverts to Community features. |
| **See pricing** | Direct link to the pricing page; visible on Community or when an existing license has expired. |
+Lemon Squeezy manages billing for Admiral subscriptions; the customer portal link and invoice history live there, not in Sencho.
+
See [Licensing & Billing](/features/licensing) for the full walkthrough including trial activation.
---
@@ -295,11 +314,15 @@ Click **Save settings** to apply.
**Scope:** Per-node (applies to the currently selected node)
-
+
### Mesh data plane
+
@@ -380,6 +403,8 @@ Quick reference:
Configure external destinations for alert notifications. Four agent types are available on separate tabs: **Discord**, **Slack**, **Webhook**, and **Apprise**. The masthead publishes a **CHANNELS** pill showing how many agents are enabled (for example, `2/4`).
+**Delivery retries** (admin-only) sets how many extra in-process attempts (0 to 3, default 0) this node makes after a transient channel failure, with a fixed one-second delay between attempts. There is no durable queue; ambiguous network failures can produce duplicate notifications.
+
For each agent:
| Field | Description |
@@ -513,7 +538,7 @@ See [Webhooks](/features/webhooks) for the full walkthrough including authentica
**Scope:** Per-node
-Create, edit, and delete labels used to organize and filter stacks across your fleet. Each label has a name and one of ten available colors. Up to 100 labels per node. The masthead publishes a **LABELS** pill with the current and maximum counts.
+Create, edit, and delete labels used to organize and filter stacks across your fleet. Each label has a name and one of ten available colors. Up to 50 labels per node. The masthead publishes a **LABELS** pill with the current and maximum counts.
See [Stack Labels](/features/stack-labels) for the full walkthrough.
@@ -700,7 +725,7 @@ Links to help resources, with an additional channel for Admiral operators.
| Channel | Tier | Description |
|---------|------|-------------|
-| **Priority Email Support** | Admiral | Direct email support during business hours (Mon–Fri 09:00–17:00 America/New_York). We aim to first-respond within one business day. This is an operational target, not an SLA. |
+| **Priority Email Support** | Admiral | Direct email support during business hours (Mon–Fri 09:00–17:00 America/New_York). We aim to first-respond within one business day. This is not a contractual SLA or 24/7 service. |
Community operators see an upgrade callout with a link to the pricing page in place of the support channels.
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b3c42507..6ce1c1de 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -4000,9 +4000,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
- "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
+ "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/frontend/package.json b/frontend/package.json
index 2b5d5809..e25e1c0c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -64,7 +64,8 @@
},
"overrides": {
"dompurify": "^3.4.11",
- "@babel/core": "^7.29.6"
+ "@babel/core": "^7.29.6",
+ "brace-expansion": "^5.0.7"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
diff --git a/frontend/src/components/settings/AppStoreSection.tsx b/frontend/src/components/settings/AppStoreSection.tsx
index 8780e6af..4f69e03a 100644
--- a/frontend/src/components/settings/AppStoreSection.tsx
+++ b/frontend/src/components/settings/AppStoreSection.tsx
@@ -5,10 +5,13 @@ import { Skeleton } from '@/components/ui/skeleton';
import { toast } from '@/components/ui/toast-store';
import { apiFetch } from '@/lib/api';
import { useAuth } from '@/context/AuthContext';
+import { useNodes } from '@/context/NodeContext';
import { RefreshCw } from 'lucide-react';
import { SettingsSection } from './SettingsSection';
import { SettingsField } from './SettingsField';
import { SettingsActions, SettingsPrimaryButton } from './SettingsActions';
+import { useNodeSettingsLoad } from './useNodeSettingsLoad';
+import { SettingsLoadGate } from './SettingsLoadError';
function SectionSkeleton() {
return (
@@ -21,33 +24,30 @@ function SectionSkeleton() {
export function AppStoreSection() {
const { isAdmin } = useAuth();
+ const { activeNode } = useNodes();
const readOnly = !isAdmin;
const [templateRegistryUrl, setTemplateRegistryUrl] = useState('');
const serverUrl = useRef('');
- const [isLoading, setIsLoading] = useState(false);
+ const { phase, isCurrentNodeLoaded, load, isSaveOwner, captureSaveGuard } = useNodeSettingsLoad(activeNode?.id);
const [isSavingRegistry, setIsSavingRegistry] = useState(false);
useEffect(() => {
- const fetchSettings = async () => {
- setIsLoading(true);
- try {
- const res = await apiFetch('/settings');
- if (res.ok) {
- const data: Record