diff --git a/README.md b/README.md
index e4c6d1c2..6953500f 100644
--- a/README.md
+++ b/README.md
@@ -17,8 +17,12 @@
[](https://github.com/studio-saelix/sencho/releases)
[](https://hub.docker.com/r/saelix/sencho)
[](https://github.com/studio-saelix/sencho/actions/workflows/ci.yml)
+ [](https://github.com/studio-saelix/sencho/actions/workflows/codeql.yml)
[](LICENSE)
- [](https://github.com/studio-saelix/sencho/discussions)
+ [](https://github.com/studio-saelix/sencho/commits/main)
+ [](https://github.com/studio-saelix/sencho/issues)
+ [](https://sencho.io)
+ [](https://docs.sencho.io)
@@ -35,11 +39,11 @@
## What Sencho is
-Sencho is for homelab operators, small DevOps teams, and platform engineers who run services on Docker Compose, want a graphical interface without giving up file-on-disk workflows, and need to manage more than one machine without SSH gymnastics or a VPN.
+Sencho is a Docker Compose control plane for DevOps engineers, platform teams, and system administrators who run services on Compose and need a real operational surface: a graphical interface that does not give up file-on-disk workflows, and the ability to manage more than one machine without SSH gymnastics or a VPN.
-It runs as a single container on your hardware and gives you a UI for the work you currently do over SSH on compose stacks: deploying, editing files, watching logs, restarting containers, browsing volumes, and recovering from failures. Your compose files stay on the host filesystem and remain the source of truth.
+It runs as a single container on your hardware and provides a UI for common Compose operations: deploying, editing files, watching logs, restarting containers, browsing volumes, and recovering from failures. Your compose files stay on the host filesystem and remain the source of truth.
-A Sencho instance is autonomous. To manage another machine, you install a second Sencho on it and connect them with a long-lived API token; the primary dashboard then acts as an authenticated HTTP and WebSocket proxy across your fleet. Use TLS, a VPN, or a private network for any untrusted link. Each node still uses its local Docker socket (see Quick start), but Sencho does not require SSH and does not expose a remote Docker socket on the network. For nodes behind NAT or strict firewalls, the Pilot Agent establishes a single outbound WebSocket tunnel to the primary, so the remote host opens no inbound port at all.
+Multi-node was part of the architecture from the start, not bolted on later: every Sencho instance is the same autonomous node, whether it runs alone or as one of many in a fleet. To manage another machine, you install a second Sencho on it and connect them with a long-lived API token; the primary dashboard then acts as an authenticated HTTP and WebSocket proxy across your fleet. Use TLS, a VPN, or a private network for any untrusted link. Each node still uses its local Docker socket (see Quick start), but Sencho does not require SSH and does not expose a remote Docker socket on the network. For nodes behind NAT or strict firewalls, the Pilot Agent establishes a single outbound WebSocket tunnel to the primary, so the remote host opens no inbound port at all.
Sencho is free, open-source software under AGPLv3. Everything below is included in the Community tier with unlimited nodes and users.
@@ -48,15 +52,18 @@ Sencho is free, open-source software under AGPLv3. Everything below is included
## Capabilities
### Stacks
-- Full Compose lifecycle: create, deploy, restart, stop, pull
+- Full Compose lifecycle: create, deploy, restart, stop, take down, pull
- Atomic deployments with automatic rollback on failure
- Monaco editor with diff preview before save and one-click rollback to any prior deploy
- [Health-gated updates](https://docs.sencho.io/features/health-gated-updates) that hold a rollout until health checks pass, with stalled-update detection and in-app recovery
- [Git-sourced stacks](https://docs.sencho.io/features/git-sources) pulled and synced from any repository, with ordered multi-file Compose
- [File explorer](https://docs.sencho.io/features/stack-file-explorer) for compose, env, and supporting files, with move and rename across directories
+- [Drift detection](https://docs.sencho.io/features/stack-drift) that compares running containers against the effective Compose model and flags exactly what changed
+- [Environment and secrets guardrails](https://docs.sencho.io/features/environment-guardrails) that inventory every variable a stack uses and flag missing or duplicate values, without ever exposing a value
+- [Storage portability](https://docs.sencho.io/features/compose-storage) checks that show whether a stack's mounts can move cleanly to another node before you move it
- [Compose Doctor](https://docs.sencho.io/features/compose-doctor) preflight checks that catch compose problems before deploy
- [Stack labels](https://docs.sencho.io/features/stack-labels) for grouping and bulk operations
-- [App Store](https://docs.sencho.io/features/app-store) with LinuxServer.io templates
+- [App Store](https://docs.sencho.io/features/app-store) with LinuxServer.io templates by default, or any custom Portainer-compatible registry
### Observability
- Aggregated [log search and stream](https://docs.sencho.io/features/global-observability) across every container in the fleet
@@ -73,6 +80,8 @@ Sencho is free, open-source software under AGPLv3. Everything below is included
- [Fleet Federation](https://docs.sencho.io/features/fleet-federation): cordon nodes and pin Blueprints to specific hosts
- [Fleet Actions](https://docs.sencho.io/features/fleet-actions): bulk label operations, fleet-wide stop-by-label, and fleet-wide prune
- [Fleet Dossier](https://docs.sencho.io/features/fleet-dossier): export the whole fleet as a single browsable Markdown archive
+- [Docker Label Audit](https://docs.sencho.io/features/docker-label-audit) across every node, for labels that drive external automation
+- [Remote updates](https://docs.sencho.io/features/remote-updates): pull the latest image and recreate any node in the fleet from the Fleet view, no SSH session required
- Node labels and grouping
- [Pilot Agent](https://docs.sencho.io/features/pilot-agent) for nodes behind NAT or strict firewalls
- Node compatibility checks before deploying
@@ -91,21 +100,22 @@ Sencho is free, open-source software under AGPLv3. Everything below is included
- [Security overview](https://docs.sencho.io/features/security) with a chart-led scan summary, sortable images, and searchable scan history
- [Vulnerability scanning](https://docs.sencho.io/features/vulnerability-scanning) via Trivy, with on-demand node-wide scans, VEX-based suppression, SARIF export, and SBOM upload
- [Compose network inspector](https://docs.sencho.io/features/compose-networking) with an exposure-intent guard for unintended published ports
-- Scan policy packs for reusable security rules
+- Node-wide network inventory, topology, and exposure findings across every stack on a node
+- [Scan policies](https://docs.sencho.io/features/vulnerability-scanning#scan-policies) that set severity thresholds and can block a deploy
- [Private registries](https://docs.sencho.io/features/private-registries) for Docker Hub, GHCR, and custom registries, plus [deploy enforcement](https://docs.sencho.io/features/deploy-enforcement) for non-compliant images
- [API tokens](https://docs.sencho.io/features/api-tokens) for automation
### Operations
- Off-site stack archives via [custom S3-compatible storage](https://docs.sencho.io/operations/backup)
-- [Notification routing](https://docs.sencho.io/features/alerts-notifications#notification-routing) to Slack, Discord, email, and webhooks
-- [Global search](https://docs.sencho.io/features/global-search) across stacks, containers, and services
+- [Notification routing](https://docs.sencho.io/features/alerts-notifications#notification-routing) to Slack, Discord, and any generic webhook
+- [Global search](https://docs.sencho.io/features/global-search) across pages, nodes, and every stack in the fleet
- [Resources view](https://docs.sencho.io/features/resources) for images, volumes, and networks with scoped prune actions
---
### Before you install
-Sencho talks to Docker through the host's `/var/run/docker.sock`. Mounting this socket grants Sencho the same privilege as `sudo docker` on the host. This is the same model used by Portainer, Dockge, Komodo, and other Compose dashboards. If your threat model requires stricter isolation, see [running with a non-root container user](https://docs.sencho.io/operations/self-hosting#non-root-user) and front Sencho with a reverse proxy that enforces authentication.
+Sencho talks to Docker through the host's `/var/run/docker.sock`. Mounting this socket grants Sencho the same privilege as `sudo docker` on the host. This is the same model used by Portainer, Dockge, Komodo, and other Compose dashboards. If your threat model requires stricter isolation, see [running with a non-root container user](https://docs.sencho.io/getting-started/configuration#container-user) and front Sencho with a reverse proxy that enforces authentication.
## Quick start
@@ -191,6 +201,7 @@ Sencho does not emit telemetry, analytics, or crash reports, and makes no outbou
## Documentation, community, and license
- **Documentation:** [docs.sencho.io](https://docs.sencho.io)
+- **Blog:** [sencho.io/blog](https://sencho.io/blog)
- **Known limitations:** [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md)
- **If something breaks:** the [Recovery guide](https://docs.sencho.io/operations/recovery) covers getting back to a working state when Sencho, a deploy, sign-in, Docker, or a node fails.
- **Community:** [GitHub Discussions](https://github.com/studio-saelix/sencho/discussions)
diff --git a/docs/docs.json b/docs/docs.json
index 64928a48..29171af0 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -76,6 +76,9 @@
"display": "simple"
}
},
+ "interaction": {
+ "drilldown": false
+ },
"navigation": {
"tabs": [
{
@@ -83,17 +86,16 @@
"groups": [
{
"group": "Start here",
- "root": "getting-started/introduction",
- "directory": "card",
"pages": [
+ "getting-started/introduction",
"getting-started/quickstart",
"getting-started/configuration"
]
},
{
"group": "Product guide",
- "root": "features/overview",
"pages": [
+ "features/overview",
{
"group": "Core workflow",
"expanded": true,
@@ -184,9 +186,8 @@
},
{
"group": "Operations",
- "root": "operations/self-hosting",
- "directory": "accordion",
"pages": [
+ "operations/self-hosting",
"operations/upgrade",
"operations/backup",
"operations/recovery",
@@ -199,9 +200,8 @@
},
{
"group": "Reference",
- "root": "reference/settings",
- "directory": "accordion",
"pages": [
+ "reference/settings",
"features/licensing",
"reference/security",
"reference/contact"
diff --git a/docs/features/api-tokens.mdx b/docs/features/api-tokens.mdx
index 59467e2e..d81a5056 100644
--- a/docs/features/api-tokens.mdx
+++ b/docs/features/api-tokens.mdx
@@ -21,7 +21,7 @@ An API token is an opaque secret value (not a JWT), 56 characters long, that Sen
The three scopes form a ladder:
- **Read Only** authorises every `GET` request the underlying API exposes.
-- **Deploy Only** adds the six stack-lifecycle POSTs: deploy, down, restart, stop, start, update.
+- **Deploy Only** adds the eight stack-lifecycle POSTs: deploy, down, restart, stop, start, update, plus per-service update and restore.
- **Full Admin** adds every other read and write on the underlying API.
A separate layer sits on top: a set of universal restrictions that **no** API token can reach, regardless of scope or HTTP method. These cover identity, trust roots, billing, and interactive shell access; even a Read Only `GET` against `/api/users` or `/api/sso/config` is rejected. They require a human user session.
@@ -145,7 +145,7 @@ Authorises **every** `GET` against the API. Representative uses: list stacks, fe
### Deploy Only
-Authorises every `GET`, plus exactly six `POST` patterns that operate on a stack by name:
+Authorises every `GET`, plus eight `POST` patterns that operate on a stack (or one of its declared services) by name:
| Method | Path pattern | Action |
|--------|--------------|--------|
@@ -155,6 +155,8 @@ Authorises every `GET`, plus exactly six `POST` patterns that operate on a stack
| `POST` | `/api/stacks/:name/stop` | Stop without removing. |
| `POST` | `/api/stacks/:name/start` | Start a stopped stack. |
| `POST` | `/api/stacks/:name/update` | Pull newer images and redeploy. |
+| `POST` | `/api/stacks/:name/services/:service/update` | Update or rebuild one declared Compose service without recreating its siblings. |
+| `POST` | `/api/stacks/:name/services/:service/restore` | Roll a single service back to a prior recovery snapshot. |
Any other write (creating a stack, editing a stack file, calling a non-lifecycle action) returns `403 SCOPE_DENIED`.
@@ -190,6 +192,7 @@ Regardless of scope, every API token is rejected from the following routes with
| **Host-console session token** | Mint the short-lived ticket the browser console uses |
| **API token self-management** | Create, list, revoke API tokens |
| **Recovery Vault** | Configure or trigger Recovery Vault |
+| **Image channel management** | Preflight or switch the instance's image channel, acknowledge a failed switch |
The rationale is the same in every case: a programmatic credential should not be able to grant itself more authority, change the trust roots that issued it, or open an interactive shell. Those actions require a live human user logged into a browser.
@@ -337,8 +340,8 @@ Read Only is sufficient for the notification stream.
The scope does not authorise the method or path you called. Either widen the scope by issuing a new token, or change the call. Common cases:
- Read Only attempting a `POST` of any kind.
- - Deploy Only attempting a write that is not one of the six lifecycle actions (`deploy`, `down`, `restart`, `stop`, `start`, `update`).
- - Any scope attempting a [universal restriction](#universal-restrictions) (user management, SSO, MFA, license, registries, console, node management, cloud backup, API-token self-management).
+ - Deploy Only attempting a write that is not one of the eight lifecycle actions (`deploy`, `down`, `restart`, `stop`, `start`, `update`, and the per-service `update`/`restore`).
+ - Any scope attempting a [universal restriction](#universal-restrictions) (user management, SSO, MFA, license, registries, console, node management, cloud backup, image channel management, API-token self-management).
diff --git a/docs/features/app-store.mdx b/docs/features/app-store.mdx
index ee455341..eafdb36c 100644
--- a/docs/features/app-store.mdx
+++ b/docs/features/app-store.mdx
@@ -45,23 +45,23 @@ Tiles are sorted by GitHub star count, descending. Click any tile to open the de
On small screens, the App Store adapts to a single-column layout. The masthead shows the store label, the total app count, the category count, and an action menu for navigation. Templates are listed alphabetically rather than by star count, and the category sidebar and featured hero are not shown on mobile.
-
+
## 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.
+
+
+ With the bundled LinuxServer.io registry, these three baseline variables are currently the only ones that appear in this section, regardless of the app: template-specific variables (a password, an API key, a subdomain) are not pulled through from that registry today. Check the app's own documentation (linked from its GitHub page) for the variables it needs and set them yourself using **Custom variables** below. A custom registry that supplies its own `env` array in its template JSON is not affected by this and populates the section normally.
+
#### Custom variables
@@ -147,7 +151,7 @@ By default Sencho uses LinuxServer.io's hosted catalogue. To point at your own:
2. Type your catalogue URL into **Registry URL**. The URL must start with `http://` or `https://` and serve a JSON array of template objects in the [Portainer v2 template format](https://documentation.portainer.io/v2.0-be/api/templates/).
3. Click **Save & refresh**. Sencho persists the new URL and immediately invalidates the template cache so the App Store reloads from the new source.
-A small chip under the input reads **using custom registry** when an override is set and **using default** when the field is empty. Click **Reset to default** to clear the override and fall back to LinuxServer.io.
+A hint next to the **Save & refresh** button reads **using custom registry** when an override is set and **using default** when the field is empty. Click **Reset to default** to clear the override and fall back to LinuxServer.io.
The default registry display at the top of the page is informational only; you cannot remove the LinuxServer.io fallback, and Sencho uses it whenever the custom field is empty.
@@ -166,6 +170,9 @@ The default registry display at the top of the page is informational only; you c
The **Scan images for vulnerabilities after deploy** checkbox renders only when the active node reports that Trivy is installed. Configure scanning per the [Vulnerability Scanning](/features/vulnerability-scanning) page, then reopen the deployment sheet. Even with the checkbox missing, the rest of the Advanced tab works exactly the same; you just lose the post-deploy scan trigger.
+
+ Some apps require a variable Sencho doesn't pre-fill for you, most commonly a password (code-server's `PASSWORD`, for example). With the bundled LinuxServer.io registry, the Environment Variables section only ever shows the PUID/PGID/TZ baseline, not the app's own declared variables. Check the app's GitHub page (linked from its LinuxServer.io listing) for the full variable list, then add what you need through **Custom variables** on the Advanced tab before deploying. If you skip a required variable like a password, the app typically starts with no authentication rather than failing to start, so add it up front rather than discovering the gap afterward.
+
## Related
diff --git a/docs/features/appearance.mdx b/docs/features/appearance.mdx
index 61449a9e..c2f5e32b 100644
--- a/docs/features/appearance.mdx
+++ b/docs/features/appearance.mdx
@@ -1,9 +1,9 @@
---
title: Appearance
-description: Personalize Sencho's look. Choose a visual style, tune readability and the security charts, pick a theme and accent, swap the interface and data fonts, and set the text size. Every choice is saved to the current browser.
+description: Personalize Sencho's look. Choose a visual style, tune the security charts and readability, pick a theme and accent, swap the interface and data fonts, and set the text size. Every choice is saved to the current browser.
---
-**Settings → Appearance** is where you make Sencho look the way you want. Choose between the Calm and Signature visual styles, dial in readability, set how the security charts are colored, pick a theme and accent, choose the interface and data fonts, and scale the text. A live preview reflects every change as you make it.
+**Settings → Appearance** is where you make Sencho look the way you want. Choose between the Calm and Signature visual styles, set how the security charts are colored, dial in readability, pick a theme and accent, choose the interface and data fonts, and scale the text. A live preview reflects every change as you make it.
@@ -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.
- The health probe is a 3-second window after `docker compose up -d` returns. Crashes after that window are out of scope for atomic rollback, because Sencho cannot tell a late exit apart from a normal restart. The [health gate](/features/health-gated-updates) covers exactly this period: it observes the stack for a configurable window after the update, records a verdict on the stack timeline, and offers a manual rollback when containers do not stay healthy. For ongoing health beyond that, use **Auto-Heal Policies** to restart unhealthy containers automatically and **Alert Rules** to page you when a container exits unexpectedly.
+ The health probe is a 3-second window after `docker compose up -d` returns. Crashes after that window are out of scope for atomic rollback, because Sencho cannot tell a late exit apart from a normal restart. The [health gate](/features/health-gated-updates) covers exactly this period: it observes the stack for a configurable window after the update, records a verdict on the stack timeline, and offers a manual rollback when containers do not stay healthy. For ongoing health beyond that, use [Auto-Heal Policies](/features/auto-heal-policies) to restart unhealthy containers automatically and [per-stack alert rules](/features/alerts-notifications#per-stack-alert-rules) to page you when a container exits unexpectedly.
This message means the auto-rollback attempted to restore the backup and re-deploy, but the restore step or the re-deploy itself errored out. The backup files are still at `/backups///`. To recover:
diff --git a/docs/features/audit-log.mdx b/docs/features/audit-log.mdx
index 93ff71a8..e1e4c633 100644
--- a/docs/features/audit-log.mdx
+++ b/docs/features/audit-log.mdx
@@ -4,7 +4,7 @@ description: Track every mutating action on your Sencho instance with a searchab
---
- Community keeps a rolling 14-day recent-activity audit API window. The Audit navigation view, CSV and JSON export, anomaly detection, and configurable retention beyond the recent window require a Sencho **Admiral** license.
+ Community keeps a rolling 14-day recent-activity audit API window, but has no **Audit** tab in navigation to browse it. The Audit navigation view, CSV and JSON export, anomaly detection, and configurable retention beyond the recent window all require a Sencho **Admiral** license.
@@ -37,10 +37,11 @@ Expanding a row in the Table view reveals additional detail:
### Example actions tracked
-- Stack lifecycle: deploy, stop, start, restart, update, rollback, delete
+- Stack lifecycle: deploy, take down, start, stop, restart, update, rollback, delete
- Stack creation, file edits, env file changes, label changes
- Per-service lifecycle (start, stop, restart of an individual compose service)
- Container operations: start, stop, restart
+- Label management: create, update, delete, and label-based bulk actions
- Node management: add, update, delete, cordon, uncordon
- Fleet operations: backup creation, restore, deletion, single-node and fleet-wide updates
- Fleet replica role changes (re-anchor, demote to control)
@@ -48,10 +49,11 @@ Expanding a row in the Table view reveals additional detail:
- Blueprint federation pin updates
- Recovery Vault: config update, connection test, provisioning, snapshot upload, snapshot deletion
- User management: create, update, delete, role assignment and removal
-- Password changes and node token generation
+- Password changes, two-factor authentication reset, and node token generation
- License activation and deactivation
- Webhook and notification agent configuration
- Notification route management and testing
+- Notification suppression rule management
- SSO configuration changes and connection tests
- API token creation and revocation
- Registry credential management
@@ -64,9 +66,9 @@ Expanding a row in the Table view reveals additional detail:
## Viewing the audit log
-The **Audit** view in navigation is an Admiral governance surface: it appears for users whose role grants the `system:audit` permission (by default **Admin** or **Auditor**). Community still has a rolling **14-day recent-activity** audit API window; Admiral adds the full Audit view, statistics, export, anomaly annotations, and configurable retention beyond that window.
+The **Audit** view in navigation is an Admiral governance surface: the tab itself only appears on an Admiral license, and Community accounts have no navigation entry point to Audit at all, though the underlying recent-activity API stays reachable directly. On Admiral, the tab is further limited to users whose role grants the `system:audit` permission, which by default means **Admin** or **Auditor**.
-Navigate to the **Audit** tab in the sidebar when it is available for your role and plan. On Admiral the feed in that view is scoped to the last 14 days by default unless you configure longer retention; export and anomaly detection come with Admiral.
+Navigate to the **Audit** tab in the top navigation when it is available for your role and plan. The feed then shows your full retained history rather than any fixed lookback window; retention defaults to 90 days and is configurable up to 365.
The page has two views, toggled from the segmented control in the card header: **Stream** (default) and **Table**. The card subtitle reports the total number of entries that match the current filters.
@@ -75,7 +77,7 @@ The page has two views, toggled from the segmented control in the card header: *
Stream gives you an at-a-glance read on activity. A signal rail at the top summarizes the last 24 hours across four tiles, and the feed below groups entries by day with severity dots, relative times, and inline anomaly callouts.
-
+
**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
+
+ Assigning the **Auditor** role requires a Sencho **Admiral** license; Community can assign **Admin** or **Viewer** only.
+
+
The **Auditor** role provides read-only access to the audit log without granting any administrative privileges. Auditors can:
- View the full audit log
@@ -166,12 +172,12 @@ On Admiral, audit log entries are automatically pruned based on your configured
To change the retention period:
-1. Open **Settings · Operations · Developer Diagnostics**
-2. In the **Data retention** card, set the **Audit log** value (1 to 365 days)
+1. Open **Settings · Operations · Data Retention**
+2. Set the **Audit log** value (1 to 365 days)
3. Click **Save settings**
-
+
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
-
- The Audit tab is an Admiral governance view shown to users whose role grants the `system:audit` permission. By default that means **Admin** or **Auditor**. If you are signed in as a Deployer or Viewer, ask an admin to assign you the Auditor role from **Settings · Users**. Community still has a rolling 14-day recent-activity audit API window; export, anomaly detection, and retention beyond the recent window come with Admiral.
+
+ The Audit tab is an Admiral governance view: it only appears on an Admiral license, and on Community it is not shown at all, regardless of role. On Admiral, the tab is further limited to users whose role grants the `system:audit` permission, by default **Admin** or **Auditor**. If you are on Admiral but signed in as a Deployer or Viewer, ask an admin to assign you the Auditor role from **Settings · Users**. The recent-activity API stays reachable on Community even without a navigation entry point to it.
Filters live in **Table view only**. Toggle the segmented control in the card header from **Stream** to **Table** and the search box, method dropdown, and From / To date pickers will appear above the grid. Switching back to Stream clears the filter strip but does not remember the last filter.
diff --git a/docs/features/auto-heal-policies.mdx b/docs/features/auto-heal-policies.mdx
index 07970cc2..1b978208 100644
--- a/docs/features/auto-heal-policies.mdx
+++ b/docs/features/auto-heal-policies.mdx
@@ -20,7 +20,7 @@ Policies live next to your stack-level alert rules in the stack's **Monitor** sh
## Prerequisites
- For **healthcheck-based** healing, containers must declare a `HEALTHCHECK` in the Dockerfile or a `healthcheck` block in `docker-compose.yml` so they report a Docker health status. **Crash-based** healing needs no healthcheck: any container that exits with a non-zero code qualifies.
-- You must be signed in as an admin.
+- Viewing existing policies and their history is open to every signed-in role. Creating, toggling, or deleting a policy requires the admin role.
Crash healing acts on crashes Sencho observes while it is running. A container that crashed before Sencho started is left for you to inspect rather than restarted automatically.
@@ -34,7 +34,7 @@ Policies live next to your stack-level alert rules in the stack's **Monitor** sh
4. Click **Add Policy**. The new policy appears in **Active policies** above the form, with its enable toggle already on.
-
+
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.
-
- Configuring Auto-Heal Policies requires an admin sign-in. If the **Auto-heal** tab is not present on the **Monitor** sheet, confirm you are signed in as an admin.
+
+ The **Auto-heal** tab itself is visible to every signed-in role, including viewers, so you can always check what is configured and browse history. Creating a policy, toggling it on or off, and deleting it are admin-only actions; a non-admin role sees the policy list and its history but not those controls. Confirm you are signed in as an admin under **Settings → Users** if you expect to make changes.
diff --git a/docs/features/auto-update-policies.mdx b/docs/features/auto-update-policies.mdx
index 9e503e55..c830a2fc 100644
--- a/docs/features/auto-update-policies.mdx
+++ b/docs/features/auto-update-policies.mdx
@@ -75,6 +75,8 @@ Once the cause is resolved, the next check (on the interval, or via **Recheck**)
On multi-service stacks, the Updates view can also apply a single service when that service has a confirmed image update. Scheduled auto-update, webhook pull, and bulk update always refresh the full stack.
5. Use **Recheck** in the hero to force an immediate registry poll across every reachable node. A 2-minute per-node cooldown applies, and the toast tells you how many nodes were triggered, rate-limited, or failed.
+**Apply now** runs the same update path as every other update trigger on a stack: Sencho takes an atomic backup of the compose and env files first, pulls the updated registry images (or rebuilds from source for services that declare `build:`, then pulls the rest), and recreates the containers. See [Controlling a running stack](/features/stack-management#controlling-a-running-stack) for exactly how a build-aware update differs from a plain pull. Once the containers are up, the [post-update health gate](/features/health-gated-updates#the-post-update-health-gate) observes them for a configurable window and records a passed, failed, or unknown verdict on the stack's timeline, the same as a manual update from the editor toolbar or sidebar.
+
## Risk badges
| Badge | Color | When it appears |
@@ -117,6 +119,8 @@ Auto-update is a first-class action in the Schedules view. To create a recurring
The task lives alongside restart, prune, snapshot, and scan tasks in the same timeline and table. Run history, notifications, and the Run Now button behave the same as for every other scheduled action. See [Scheduled Operations](/features/scheduled-operations) for details.
+A scheduled run applies an update the same way **Apply now** does: an atomic backup first, then the pull or build-aware rebuild, then the [post-update health gate](/features/health-gated-updates) observing the result. A schedule never bypasses a [deploy enforcement](/features/deploy-enforcement) policy: a run that a policy would block fails with the policy violation recorded in the run history, rather than applying partway.
+
## Multi-node support
The readiness board shows pending updates from every node in your fleet in a single view, regardless of which node is selected in the sidebar. You do not need to switch nodes to inspect what is pending elsewhere.
@@ -127,7 +131,7 @@ Each node group renders its own card grid under a header showing the node name,
For each stack with a pending image update, Sencho computes a preview by:
-1. Parsing the compose file to enumerate every pullable image reference.
+1. Parsing the compose file to enumerate every service that pulls a registry image (a service declaring only `build:`, with no `image:` key, has no registry reference to check and is excluded from this preview).
2. Calling the registry with your configured credentials to fetch the current tag list and remote digest.
3. Picking the highest semver tag greater than the current tag (keeping the same prefix and suffix). If the highest available tag matches the current one but the remote digest has changed, the card surfaces as a `Rebuild available` update. Sencho checks both a newer semver tag and a digest change behind the same tag; see [Tags vs digests](/features/vulnerability-scanning#tags-vs-digests).
4. Scoring the overall stack by the most severe image bump. Any major bump marks the stack as blocked.
@@ -135,6 +139,8 @@ For each stack with a pending image update, Sencho computes a preview by:
The preview is recomputed each time the readiness board loads, so it reflects the live state of your registries and local images.
+A stack that mixes registry images and `build:` services still gets a card, scored from its registry images. Triggering **Update** on that stack, from this board or a schedule, always rebuilds the `build:` services from source in the same run, whether or not the registry portion has a pending update. See [Controlling a running stack](/features/stack-management#controlling-a-running-stack) for what a build-aware update runs.
+
## Troubleshooting
@@ -162,4 +168,30 @@ The preview is recomputed each time the readiness board loads, so it reflects th
Automatic cleanup reclaims dangling (untagged) image layers only. A long-running host accumulates other reclaimable data (stopped containers, unused volumes and networks, build cache) that this toggle does not touch. Run a full prune from the [Resources](/features/resources) view, or schedule a prune task, to reclaim the rest.
+
+ A [deploy enforcement](/features/deploy-enforcement) policy blocked the update, the same way it would block a manual one. The run history and the dispatched alert name the policy and the matching image findings. Review the findings and either clear them or adjust the policy, then let the next scheduled run or a manual **Apply now** retry.
+
+
+## Related
+
+
+
+ The readiness check before an update and the post-update health gate that every apply path, including auto-update, runs through.
+
+
+ How Sencho backs up compose and env files before each update and rolls back automatically on failure.
+
+
+ Set risk policies that block or require review before a manual or scheduled update can apply.
+
+
+ The full set of lifecycle actions on a stack, including how a build-aware update differs from a plain pull.
+
+
+ How the Auto-update Stack and Auto-update All Stacks on Node tasks fit into the shared scheduling system.
+
+
+ Why a same-tag digest change surfaces as a rebuild instead of a version diff.
+
+
diff --git a/docs/features/blueprint-model.mdx b/docs/features/blueprint-model.mdx
index 29a8326f..223fcf6e 100644
--- a/docs/features/blueprint-model.mdx
+++ b/docs/features/blueprint-model.mdx
@@ -8,7 +8,7 @@ A **Blueprint** bundles a `docker-compose.yml` with a node selector and a drift
Blueprints live under **Fleet · Deployments**.
- Blueprints are available on every tier as Sencho's Compose-first orchestration model. Creating, editing, withdrawing, and applying blueprints requires an admin role; operators and viewers can read the catalog and the detail sheet. Pinning a blueprint to a specific node lives under **Fleet · Federation** on every tier (admin role required).
+ Blueprints are Sencho's Compose-first orchestration model. Creating, editing, withdrawing, and applying blueprints requires an admin role; operators and viewers can read the catalog and the detail sheet. Pinning a blueprint to a specific node lives under **Fleet · Federation** (admin role required).
@@ -47,7 +47,6 @@ Drift detection runs on every tick for every Active deployment regardless of pol
| Requirement | Detail |
|---|---|
-| License tier | **Community** to read, create, edit, withdraw, apply, and pin blueprints. |
| User role | **Admin** to create, edit, withdraw, accept, and apply. Operators and viewers can read the catalog and the detail sheet. Pinning requires admin. |
| Nodes | At least one node that the selector resolves to. Remote nodes need a healthy proxy connection; see [Multi-node management](/features/multi-node) and [Pilot Agent](/features/pilot-agent) for enrollment. |
| Compose YAML | Valid `docker-compose.yml`, 96 KiB or fewer. |
@@ -88,8 +87,8 @@ Before a local Blueprint deploy starts, Sencho applies the same pre-deploy [vuln
**New Blueprint** opens a single editor dialog with everything inline: name and description fields, a Monaco YAML editor with the classification banner sitting above it, the selector picker (Labels or Specific nodes), the drift policy cards, and the Reconciler enabled toggle.
-
-
+
+
### 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.
- Confirm the drift policy is **Enforce** and that **Reconciler enabled** is on. Open the detail sheet to check the row's status and the most recent drift summary. If the drift was caused by a compose change that would destroy named volumes, Enforce intentionally downgrades to Suggest semantics for that event. Either change the compose to one that preserves the volumes, or withdraw the deployment with explicit operator confirmation and let the new revision deploy fresh.
+ Confirm the drift policy is **Enforce** and that **Reconciler enabled** is on. Open the detail sheet to check the row's status and the most recent drift summary. If the row is sitting in **Awaiting confirmation** instead of **Drifted**, that is expected: a compose edit on a stateful or state-unknown blueprint never auto-corrects under any drift mode, including Enforce. It always waits for an explicit **Confirm deploy** so a human signs off before the new revision touches that node's data. Enforce's auto-fix only applies to container-level drift (a stopped or crashed container) on a deployment that is already **Active**.
**Apply now** requires the blueprint to be enabled. Open the detail sheet, click **Enable** to flip the reconciler back on, then click **Apply now**. The button is also disabled while the detail sheet is in edit mode; save or cancel the edit first.
diff --git a/docs/features/compose-doctor.mdx b/docs/features/compose-doctor.mdx
index acbe41bc..4fdf56ac 100644
--- a/docs/features/compose-doctor.mdx
+++ b/docs/features/compose-doctor.mdx
@@ -5,7 +5,7 @@ description: Run a preflight check on a stack before you deploy. Compose Doctor
The **Doctor** tab in the right-hand **Anatomy** panel answers one question before you apply a change: *what will Docker actually run, and is it safe on this node?* Compose Doctor renders the effective Compose model (the fully resolved result after interpolation, includes, profiles, `.env`, and `env_file` are applied) and then runs a set of deterministic checks against it and the live Docker state on the node it would deploy to.
-The check is advisory. It never blocks a deploy and never changes a stack. It runs on demand: press **run preflight** and Sencho renders the model, runs all 31 checks, and stores the result so the tab still shows it the next time you open the stack.
+The check is advisory: on its own it never blocks a deploy or changes a stack. (One rule is the exception: see [Self-management](#self-management) below for the one case where Sencho actively blocks an action, independent of this report.) It runs on demand: press **run preflight** and Sencho renders the model, runs all 32 checks, and stores the result so the tab still shows it the next time you open the stack.
## Where to find it
@@ -24,7 +24,7 @@ Every preflight run follows three steps:
1. **Render** the effective model. Sencho calls `docker compose config` on the stack, which resolves all variable interpolation, `include` directives, profile overrides, and `env_file` references into a single, normalized model.
2. **Snapshot** live Docker state. Sencho reads which host ports are in use, which containers are running, and which named networks and volumes exist on the target node.
-3. **Run 31 deterministic rules** against the combination. Each rule is pure and produces zero or more findings with a severity, a message, and a suggested fix.
+3. **Run 32 deterministic rules** against the combination. Each rule is pure and produces zero or more findings with a severity, a message, and a suggested fix.
Sencho stores exactly one run per stack per node, so a new run immediately overwrites the previous one; there is no history.
@@ -54,14 +54,15 @@ The summary card at the top of the Doctor tab reflects the overall outcome of th
| **No run yet** | Preflight has never been triggered for this stack on this node | Placeholder with "no preflight yet" and a Stethoscope icon |
| **All clear** | Model rendered; no findings | Green card with "No issues found in the effective model." |
| **Cannot render** | `docker compose config` failed | Red card with the (redacted) render error message |
-| **Blocker** | Highest finding is a blocker | Red card with a count of findings by severity |
-| **High risk** | Highest finding is high risk | Amber card with a count of findings by severity |
-| **Warning** | Highest finding is a warning | Blue card with a count of findings by severity |
-| **Info** | All findings are informational | Muted card with a count of findings by severity |
+| **Blocker** | Highest active finding is a blocker | Red card with a count of active findings by severity |
+| **High risk** | Highest active finding is high risk | Amber card with a count of active findings by severity |
+| **Warning** | Highest active finding is a warning | Blue card with a count of active findings by severity |
+| **Info** | All active findings are informational | Muted card with a count of active findings by severity |
+| **Acknowledged** | Every finding from the last run has been acknowledged | Muted card noting all findings are acknowledged |
-The summary card also shows when the run happened and who triggered it: "ran 5 minutes ago by admin".
+The summary card also shows when the run happened and who triggered it: "ran 5 minutes ago by admin". If any findings are acknowledged, the line adds a count: "3 active (1 blocker · 2 warning) · 2 acknowledged".
-A small colored dot appears on the **Doctor** tab label when the last run found blockers (red) or high-risk issues (amber). Opening the tab removes neither the dot nor the findings; run preflight again after fixing the issues to clear them.
+A small colored dot appears on the **Doctor** tab label when the last run's active findings include a blocker (red) or a high-risk finding (amber). Click the **X** on the summary card to dismiss it; this also clears the tab dot. The dismissal is remembered per stack and node, and clears itself automatically the moment the finding set changes (a rule clearing, a new rule firing, or a severity change), so a stale dismissal can never hide a genuinely new problem. It never clears just by opening the tab or re-running preflight to the same result, and it does not sync across browsers or teammates: it is stored in your browser only.
Node Update** instead of the stack's own update action. To manage the stack normally (for example, to deliberately restructure it), move its compose project outside the node's configured Compose directory first.
+
## Networking-relevant findings on the Networking page
The networking-relevant rules on this page (host mode, exposure intent, port conflicts, and the exposure-intent checks above) also surface on the node-wide [Networking](/features/networking) page's Findings tab, using the result of your last run here. A finding both engines detect is merged into one card; a finding only these Doctor rules catch appears there labeled with the run's timestamp. Acknowledging a finding here also excludes it from the Networking page.
## Node-state checks and graceful degradation
-Six of the 30 rules require live Docker state to run: five are in the Node state category (external networks and volumes, new-resource notices, and container_name collision) and one is "Host port is already in use" in Port conflicts. All six are skipped when the Docker daemon is unreachable.
+Six of the 32 rules require live Docker state to run: five are in the Node state category (external networks and volumes, new-resource notices, and container_name collision) and one is "Host port is already in use" in Port conflicts. All six are skipped when the Docker daemon is unreachable.
When the daemon is unreachable:
@@ -184,13 +199,15 @@ A partial run (where node-state checks were skipped) is stored and displayed lik
## Health-Gated Updates
-The last stored preflight result feeds directly into the [Health-Gated Updates](/features/health-gated-updates) readiness check. When you trigger **Update** from a stack, Sencho opens a readiness dialog before pulling anything. The dialog reads your most recent preflight result:
+The last stored preflight result feeds directly into the [Health-Gated Updates](/features/health-gated-updates) readiness check as one of several signals. When you trigger **Update** from a stack, Sencho opens a readiness dialog before pulling anything. The dialog reads your most recent preflight result (active findings only; acknowledged findings are excluded):
-- A blocker finding sets the update verdict to **Blocked**.
-- One or more high-risk findings set the verdict to **Review required**.
-- Preflight that has never run is noted in the dialog without dragging the verdict down.
+- A blocker finding sets the update verdict to **blocked**.
+- A model that failed to render, or one or more high-risk findings, sets the verdict to **review required**.
+- Warning-only findings do not block the verdict on their own, but keep it from reading a clean **ready**; combined with an otherwise clean update they surface as **ready with warnings**.
+- A clean pass (no active findings, or only info-level ones) contributes a positive signal toward **ready**.
+- Preflight that has never run is noted in the dialog as **unknown** for that signal, without dragging the overall verdict down by itself.
-Running preflight before updating gives the readiness check the most accurate signal. A clean pass after resolving findings contributes a positive readiness signal for that stack.
+Other signals (current container health, the pending image change, the rollback backup, and disk space) are combined with this one; the dialog's overall verdict reflects the worst signal across all of them, not preflight alone. Running preflight before updating gives the readiness check the most accurate signal for this stack.
## Running preflight
@@ -228,7 +245,7 @@ Acknowledgements are stored per node, stack, rule, and service. They do not sync
@@ -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.
The summary card shows the username and relative time: "ran 5 minutes ago by admin". This reflects the most recent run stored for this stack on the currently active node.
+
+ Sencho detected that the stack you are checking is its own compose project. Generic deploy, update, stop, down, and delete actions are refused for this stack no matter what the preflight report says, because any of them could recreate or remove the dashboard you are using right now. Update Sencho from **Fleet -> Node Update** instead. See [Self-management](#self-management).
+
+
+ Dismissing the summary card (the **X** button) is stored in your browser's local storage, keyed to the stack and node. It does not sync across devices, browsers, or user accounts, so a dismissal on your machine has no effect on what anyone else sees.
+
## Related features
diff --git a/docs/features/compose-networking.mdx b/docs/features/compose-networking.mdx
index 9ee1ecc8..f1cad0bb 100644
--- a/docs/features/compose-networking.mdx
+++ b/docs/features/compose-networking.mdx
@@ -7,6 +7,8 @@ The **Networking** tab in the right-hand **Anatomy** panel answers two questions
The tab is read-only with respect to stack deployments: it never alters a container, a Compose file, or an existing network attachment. Two write operations are available: setting the stack's *exposure intent* (stored separately so Sencho can flag mismatches over time) and creating a new Docker network on the active node, which is available to admins and covered in [Creating networks](#creating-networks) below.
+This tab stays stack-scoped: it models one deployment at a time. For a node-wide inventory of every Docker network, an interactive topology graph, and cross-stack findings, see the [Networking](/features/networking) page instead. A **view node networking** link at the bottom of this tab jumps there directly.
+
Compose Networking reads structure: network names, service-to-network membership, published ports, network modes, and the *names* of environment variables and labels, never their values. A secret injected through `environment:` or `env_file:` never appears in the view or the logs. One caveat: a structural field assembled by interpolating a secret (a network `name:`, a published port, or an `extra_hosts` entry built from `${VAR}`) is resolved before the inspector reads it, so its value does show. Keep secrets in `environment:` or `env_file:` rather than interpolating them into structural fields. See [Environment and Secrets Guardrails](/features/environment-guardrails).
@@ -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
+
+ The node-wide operator page: full network inventory, an interactive topology graph, and cross-stack findings, independent of which stack they belong to.
+
Runs preflight checks against the same effective model, including the five exposure-aware findings that rely on the intents set in the Networking tab.
Shows the same four runtime drift types with a persistent history, so you can see when drift first appeared and when it cleared.
-
- Host-wide view of every Docker network and container attachment on the node, independent of which stack they belong to.
-
Documents which fields Sencho reads and which it redacts, and how to keep secrets safe from interpolation into structural fields.
diff --git a/docs/features/compose-storage.mdx b/docs/features/compose-storage.mdx
index e5ae22be..8c054a12 100644
--- a/docs/features/compose-storage.mdx
+++ b/docs/features/compose-storage.mdx
@@ -15,7 +15,7 @@ Click any stack in the left sidebar, then switch to the **Storage** tab in the A
@@ -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
Back up Compose and env files across all nodes in one action. Covers configuration, not volume data.
-
- Browse and edit the files inside a stack directory and read the contents of named volumes.
+
+ Browse and edit the files inside a stack's directory and its mounted volumes from the Files tab.
Keep secrets out of mount paths so they are not exposed in the storage inventory.
diff --git a/docs/features/cve-suppressions.mdx b/docs/features/cve-suppressions.mdx
index d108690d..ea864e02 100644
--- a/docs/features/cve-suppressions.mdx
+++ b/docs/features/cve-suppressions.mdx
@@ -21,10 +21,10 @@ Badge counts in the Resources Hub continue to reflect the raw findings. Suppress
## Creating a suppression
-Open the **Security** page → **Suppressions** tab, then click **Add Suppression**.
+Open the **Security** page → **Suppressions** tab, then click **Add suppression**. The same tab also holds a **Misconfig acknowledgements** panel for Compose-config findings; that is a related but separate feature, covered in [Misconfig acknowledgements](/features/vulnerability-scanning#misconfig-acknowledgements).
-
+
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.
- Replicas are read-only for security rules. Sign in to the control instance to add, edit, or delete suppressions; changes sync automatically. Replicated rows show a `replicated` badge and no trash icon.
+ Replicas are read-only for security rules. Sign in to the control instance to add, edit, or delete suppressions; changes sync automatically. Replicated rows show a `replicated` badge and no edit or trash icon.
Two common causes:
diff --git a/docs/features/dashboard.mdx b/docs/features/dashboard.mdx
index e6da7619..5e4ddbb0 100644
--- a/docs/features/dashboard.mdx
+++ b/docs/features/dashboard.mdx
@@ -6,7 +6,7 @@ description: Real-time system health, stack load, configuration overview, fleet
The **Home** tab is the first thing you see after logging in. It surfaces the active node's overall health, live system metrics, the load on every stack, the on/off state of every automation and security feature, a fleet- or restart-activity panel, and the running alert tape, all on a single scrollable page.
-
+
## 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 ` rules` |
-| **Notification routing** | Number of enabled routing rules that direct categories to specific agents, formatted ` routes` |
+| **Routing** | Number of enabled routing rules that direct categories to specific agents, formatted ` routes` |
+| **Mute Rules** | Number of enabled notification-suppression rules, formatted ` rules`. See [Mute Rules](/features/alerts-notifications#mute-rules). |
### Automation
@@ -106,7 +109,8 @@ The card is divided into four sections.
|-----|---------------|
| **MFA** | `On` when TOTP is configured for the signed-in operator, `Off` when configured but disabled, `Not set up` when there is no MFA secret on file |
| **SSO** | The active SSO provider name (`OIDC`, `Google`, `GitHub`, `Okta`, `LDAP`); reads `Off` when SSO is not enabled |
-| **Vulnerability scanning** | Count of enabled scan policies on the active node; reads `None` when no policy is enabled |
+| **Trivy installed** | `Installed` when a Trivy scanner binary is available on this node, `Not installed` otherwise |
+| **Scan policies** | Count of enabled vulnerability scan policies on the active node, formatted ` policies`; reads `None` when no policy is enabled |
### Backups & Thresholds
@@ -116,21 +120,21 @@ The card is divided into four sections.
| **Alert thresholds** | The current host thresholds, formatted `CPU x% · RAM y% · Disk z%`, or `Off` when host threshold alerts are disabled |
| **Crash detection** | `On` when global container-crash notifications are enabled, `Off` otherwise |
-Click any row to jump directly to the settings section that manages it. Rows that require a higher tier than the active license are not rendered. The section headers (Notifications, Automation, Security, Backups & Thresholds) only render when at least one of their rows is visible.
+Click any row to jump directly to the settings section that manages it.
-The data refreshes automatically every 60 seconds and immediately on any container start/stop/restart event broadcast over the live notification stream, so the card stays in lockstep with what the rest of the UI shows.
+The card refreshes automatically every 60 seconds. It also refreshes immediately whenever a scheduled task is created, edited, toggled, or deleted, so the **Scheduled tasks** row (and the rest of the card) reflects that change within a second. Other changes covered by this card, such as switching the cloud backup provider or enabling SSO, update on the next 60-second tick.
## Activity panel
-To the right of Configuration Status, the dashboard shows one of two activity panels depending on whether you have remote nodes registered:
+To the right of Configuration Status, the dashboard shows one of two activity panels:
-- **Fleet Heartbeat** when at least one remote node is configured.
-- **Stack Restarts (7d)** when this Sencho instance manages only the local node.
+- **Fleet Heartbeat** when at least one remote node is registered and your role has the `node:read` permission.
+- **Stack Restarts (7d)** when this Sencho instance manages only the local node, or when your role lacks `node:read` (so a deployer-level role never sees a fleet card it cannot load).
### Fleet Heartbeat
-
+
The header carries a radio-tower icon, the total node count, and a ` unreachable` callout in destructive red whenever any node is offline. Each row shows:
@@ -159,7 +163,7 @@ Stacks with zero restarts are not listed individually; they are summarized in a
The bottom-most card on the dashboard collects every triggered notification on the active node, sorted by recency.
-
+
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 ` stacks · up · dn · sync ` meta line), the CPU hero card with its sparkline, a three-up memory/disk/network strip, and a **Stack Health** list capped at six rows with a **view all →** link to the full stack list. Rows show only the stack name, its host, a 10-minute sparkline, and current CPU; tap a row to open that stack's editor.
+
+Configuration Status, the Fleet Heartbeat / Stack Restarts panel, and Recent Alerts are desktop-only; open Sencho on a wider screen to see them. Notifications stay reachable from the bell icon in the mobile masthead.
## How health is derived
@@ -201,7 +215,7 @@ The dashboard is built from several independent polling loops so that fast-movin
| Stack Restarts (7d) | 5 minutes |
| Recent Alerts | Pushed live over the notifications WebSocket, with a 60-second safety-net reconcile poll |
-In addition, every Docker container event (start, stop, die, restart, health-status change) on the active node fires a `sencho:state-invalidate` browser event that immediately refetches container counts, system stats, stack statuses, and the Configuration Status card so the masthead and the table react in well under a second instead of waiting for the next polling tick.
+In addition, every Docker container event (start, stop, die, restart, health-status change) on the active node fires a `sencho:state-invalidate` browser event that immediately refetches container counts, system stats, and stack statuses, so the masthead, gauges, and stack health table react in well under a second instead of waiting for the next polling tick. The Configuration Status card ignores these container-level events; it only reacts early to scheduled-task changes (see the section above), so it stays on its 60-second cadence for everything else.
When you switch the active node from the node switcher, the dashboard resets every panel to its loading state and starts a new round of fetches against the new node so you never see stale data from the previous one.
diff --git a/docs/features/deep-links.mdx b/docs/features/deep-links.mdx
index 4af0a2bd..875a2a49 100644
--- a/docs/features/deep-links.mdx
+++ b/docs/features/deep-links.mdx
@@ -3,7 +3,7 @@ title: Deep links and URLs
description: Bookmark, share, and refresh Sencho views with real browser URLs that survive Back and Forward.
---
-Every major screen in Sencho now has a stable address in the browser bar. Refresh the page, paste a link to a teammate, or use Back and Forward without losing your place.
+Every major screen in Sencho has a stable address in the browser bar. Refresh the page, paste a link to a teammate, or use Back and Forward without losing your place.
## What gets its own URL
@@ -16,27 +16,36 @@ Sencho encodes the active node, the view you are on, and the deep state that vie
| Stack detail | `/nodes/local/stacks/radarr` | Radarr's stack detail (anatomy). Sidebar entry point |
| Compose editor | `/nodes/local/stacks/radarr/compose` | Radarr's compose.yaml Monaco editor |
| Env tab + file | `/nodes/local/stacks/radarr/env?env=.env.prod` | Radarr's env tab with a specific env file selected |
+| Files browser | `/nodes/local/stacks/radarr/files` | Radarr's file browser (desktop; opens the compose editor instead on a phone) |
| Resources | `/nodes/local/resources` | Resources for the active node |
| Networking | `/nodes/local/networking` | Networking operator page for the active node |
| Security tab | `/nodes/local/security/images` | Security view on the Images tab |
+| App Store | `/nodes/local/templates` | App Store on the active node |
+| Logs | `/nodes/local/logs` | Cross-fleet log aggregation |
+| Update | `/nodes/local/updates` | Fleet-wide update check |
+| Console | `/nodes/local/host-console` | Host Console, a limited-availability operator surface documented on its own page when enabled on an instance |
+| Audit | `/nodes/local/audit` | Audit history |
| Settings section | `/nodes/local/settings/nodes` | Settings on the Nodes section |
| Fleet tab | `/nodes/local/fleet/snapshots` | Fleet on the Snapshots tab (desktop) |
| Schedules filter | `/nodes/local/schedules?node=3` | Scheduled operations filtered to node `3` |
+Fleet, Logs, Update, Schedules, and Audit are gated by role or license tier and are hub-only: their URLs redirect to the dashboard while a remote node is active. Console is also gated, but is reachable against any node, remote included. See [Multi-Node Fleet](/features/multi-node#what-top-level-views-show-when-a-remote-node-is-active) for the hub-only list and [RBAC & User Management](/features/rbac) for the full permission matrix.
+
+Fleet tab URLs are stable slugs that do not always match the tab's on-screen label. For example the **Status** tab uses the `configuration` segment and the **Map** tab uses the `dependencies` segment.
+
Remote nodes use a slug derived from the node name and id (for example `/nodes/nas-box-42/dashboard`). The default local node keeps the short `local` slug.
## Stack detail vs compose editor
Clicking a stack in the sidebar opens **stack detail** at `/nodes//stacks/` (anatomy on the right). Opening the compose, env, or files editor appends that tab to the path:
-- `/stacks/radarr` — detail
-- `/stacks/radarr/compose` — compose.yaml editor
-- `/stacks/radarr/env` — env editor
-- `/stacks/radarr/files` — file browser (desktop)
+- `/stacks/radarr`: detail
+- `/stacks/radarr/compose`: compose.yaml editor
+- `/stacks/radarr/env`: env editor
+- `/stacks/radarr/files`: file browser (desktop)
Closing the Monaco editor returns you to the detail URL for that stack.
-
## Env tab URLs
Sencho encodes env file selection in the `?env=` query on the Env tab only:
@@ -45,6 +54,7 @@ Sencho encodes env file selection in the `?env=` query on the Env tab only:
- **Non-default files use basenames only:** `?env=.env.prod`, never a server filesystem path.
- **Legacy bookmarks:** older links that used an absolute path in `?env=` still open the matching file by basename. Sencho normalizes the URL to basename form when the address bar updates.
- **Unknown basenames:** if `?env=` does not match any file on the stack, Sencho opens the default env file.
+- **No env files:** a stack with no env file at all has no Env tab. Opening `/env` on it stays on the compose editor instead.
Compose and Files routes never carry `?env=`.
@@ -74,7 +84,9 @@ On a phone, Home and the stack list are distinct URLs even though both relate to
On desktop, opening the stack list at `/nodes/local/stacks` canonicalizes to `/nodes/local/dashboard` because the sidebar stack list is part of the home layout on a wide screen.
-Settings follows the same list/detail split: `/nodes/local/settings` is the section list; `/nodes/local/settings/` opens a section directly.
+Settings follows a similar split, but only on a phone: `/nodes/local/settings` opens the section list, and `/nodes/local/settings/` opens a section directly. On desktop, `/nodes/local/settings` always normalizes straight to `/nodes/local/settings/appearance`; there is no bare section-list state to land on.
+
+Fleet, Resources, Security, App Store, Logs, Update, Audit, and Schedules keep the exact same URL between desktop and phone, each rendering a phone-optimized screen at that path. Console also keeps the same URL on a phone, reflowing the terminal rather than switching to a dedicated phone screen.
On a phone, `/nodes/local/stacks//files` opens the compose editor instead. Sencho does not expose a separate file-browser URL on a phone.
@@ -86,6 +98,6 @@ Some in-app state is intentionally not encoded:
## Tips
-- Use the global search palette (Ctrl+K) as today; navigation still updates the URL when you pick a page, node, or stack.
+- Use the global search palette (Ctrl+K) to jump anywhere; navigation still updates the URL when you pick a page, node, or stack.
- Deep links use stack directory names (the name you see in the sidebar), not internal compose filenames.
- Opening Sencho at `/` sends you to the active node's dashboard once you are signed in.
diff --git a/docs/features/deploy-enforcement.mdx b/docs/features/deploy-enforcement.mdx
index 970c47e6..ebeb457b 100644
--- a/docs/features/deploy-enforcement.mdx
+++ b/docs/features/deploy-enforcement.mdx
@@ -10,6 +10,8 @@ Deploy enforcement is the pre-flight half of Sencho's vulnerability workflow. Wh
Deploy enforcement and scan policies are available on every tier.
+The **Security** page's Overview tab carries a **Deploy enforcement** summary card showing the count of enabled block policies and the current Honor suppressions state. The card is read-only; manage policies from the **Policies** tab.
+
## Configuring a block policy
Policies are managed on the **Security** page → **Policies** tab. The **Add policy** button opens the editor. When no policies exist, an empty-state callout reads "No scan policies configured" with a prompt to add one. Existing policies appear as a list of cards: each card shows the policy name, a badge per active block condition (`max: ` when the severity threshold is on, `KEV`, and `Fixable`), a destructive `block` badge when the pre-flight gate is active, and a `disabled` badge when the policy is off. Below the name, the card shows `Scope:` followed by the stack-pattern glob in monospace, or `all stacks` in italics when no pattern is set. Pencil and trash buttons appear for admins on control nodes.
@@ -21,7 +23,7 @@ Policies are managed on the **Security** page → **Policies** tab. The **Add po
The editor exposes the fields that govern enforcement:
-
+
| 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 ` lines` counter while output is flowing, `Succeeded · closes in s` after a clean finish, or the failure message itself when the run errors out. If the live stream cannot attach or drops, the indicator switches to `Live progress unavailable` and the operation keeps running in the background.
+- **Header**: the action verb (Deploying, Updating, Installing, Restarting, Stopping, Taking down, Scanning), the stack name in monospace truncated at 200 px, and an elapsed-time chip that appears once the connection moves past the initial **Connecting...** state.
+- **Status indicator** in the upper right: one of `Connecting...` while the stream attaches, a live ` lines` counter while output is flowing, `Succeeded` with a `closes in s` countdown after a clean finish, or the failure message itself when the run errors out. If the live stream cannot attach or drops, the indicator switches to `Live progress unavailable` and the operation keeps running in the background.
- **Structured log body**: one row per output line. Each row carries a timestamp, a fixed-width stage badge, and the log message. Error rows have a destructive left border and tinted background so they stand out without scanning.
- **Footer**: a `Raw output` / `Hide raw` toggle on the left, with **Minimize** and **Close** buttons on the right.
@@ -48,10 +48,10 @@ When the gate passes, the success state and 4-second auto-close resume. When the
Closing or minimizing the modal never stops the observation. The gate runs to completion in the background regardless of what happens to the progress view.
-The health gate activates only for **deploy** and **update** actions. Restart, stop, and scan do not trigger it.
+The health gate activates only for **Deploy** and **Update** actions; no other action triggers it.
-
+
## 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 s` and a 4-second countdown begins. Hover anywhere over the modal to pause the countdown; moving the cursor away restarts a fresh 4 seconds. Clicking **Close** dismisses the modal immediately.
+When an action completes successfully, the status indicator switches to a green `Succeeded` label with a `closes in s` countdown next to it, and a 4-second countdown begins. Hover anywhere over the modal to pause the countdown; moving the cursor away restarts a fresh 4 seconds. Clicking **Close** dismisses the modal immediately.
When the [health gate](#health-gate) is observing the stack after a deploy or update, the modal withholds the success verdict: the status indicator shows **Verifying health** instead of **Succeeded**, and the auto-close waits for the gate result. A passed gate resumes the countdown; a failed or unknown result keeps the modal open until you close it manually.
@@ -108,10 +108,10 @@ Click **Minimize** to collapse the modal to a small status pill anchored at the
The pill is mounted as a portal, so it persists across navigation: leave the App Store mid-install and the pill follows you to the dashboard, the editor, or any other view.
-Minimizing, closing, or navigating away never cancels the operation. The progress view only displays output; the deploy, update, or stop runs to completion on its own. If you dismiss the view while live output is flowing, the run still finishes in the background, and its success or failure lands in your notifications.
+Minimizing, closing, or navigating away never cancels the operation. The progress view only displays output; the triggering action runs to completion on its own. If you dismiss the view while live output is flowing, the run still finishes in the background, and its success or failure lands in your notifications.
-
+
## 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
The modal withholds auto-close while the health gate is observing containers and whenever the gate result is failed or unknown. A failed or unknown gate means the containers did not stay healthy through the observation window; the modal stays open so you can see the reason. Use the recovery actions on the stack page to retry, restart, or roll back. Clicking **Close** on the modal dismisses it manually at any time; it does not cancel the gate observation if it is still running.
-
- The health gate only fires after **deploy** and **update** actions. Restart, stop, install, git apply, and scan complete without a post-operation observation period. If you want health monitoring after a restart, deploy or update the stack instead.
+
+ The health gate only fires after **deploy** and **update** actions. Restart, stop, take down, install, Git apply, and Scan this node complete without a post-operation observation period. If you want health monitoring after a restart, deploy or update the stack instead.
The settings live in `localStorage`: the on/off toggle under `sencho.deploy-feedback.enabled` and the Modal/Inline choice under `sencho.deploy-feedback.style`. Both apply to the current tab without a reload, and other tabs in the same browser pick up the change through a `storage` event. Deploy progress defaults to on in Modal style, so only an explicit choice changes it. If a tab still does not honour the setting, refresh that tab. The settings do not sync across browsers or devices; each one carries its own choice.
diff --git a/docs/features/docker-label-audit.mdx b/docs/features/docker-label-audit.mdx
index 3dc279e4..1957879f 100644
--- a/docs/features/docker-label-audit.mdx
+++ b/docs/features/docker-label-audit.mdx
@@ -1,10 +1,10 @@
---
title: "Docker Label Audit"
sidebarTitle: Label audit
-description: "Audit Docker and Compose labels that drive external automation across the fleet and inside each stack."
+description: "Audit Docker and Compose labels that drive external automation across your fleet and inside each stack, with source provenance and Compose-vs-runtime reconciliation."
---
-Docker labels are metadata declared on Compose services or attached to running containers. They are different from Sencho Stack Labels used for organizing stacks and Node Labels used for Blueprint placement.
+Docker labels are metadata declared on Compose services or attached to running containers. They are different from [Stack Labels](/features/stack-labels) used for organizing stacks in the sidebar and Node Labels used for Blueprint placement.
Sencho surfaces two read-only audit views for these labels:
@@ -13,7 +13,14 @@ Sencho surfaces two read-only audit views for these labels:
## Fleet Docker Labels tab
-Open **Fleet** and select the **Docker Labels** tab (after **Map**). The panel is titled **Docker label audit** and offers two layouts:
+Open **Fleet** and select the **Docker Labels** tab, which appears after **Map**. The panel is titled **Docker label audit** and offers two layouts:
+
+
+
+
| 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
+
+
+
+ This is expected. The Fleet Docker Labels tab reads only container-level metadata and has no access to the compose file, so it cannot confirm a label originated in Compose; anything it cannot attribute to the image or to Compose's own system labels is shown as **Present at runtime**. Open the stack's **Compose Labels** tab instead, which renders the compose model and correctly shows the same label under **Declared in Compose**.
+
+
+ Provenance for a label falls back to **Unknown** when Sencho could not inspect the container or its image on that node. Confirm the node's Docker daemon is reachable and that the image referenced by the container still exists locally, then press **Refresh**.
+
+
+ Both tabs require the active node to report the `container-label-inventory` capability. A node running an older version of Sencho does not advertise it, so the tab is hidden for that node until it is updated. See [Capability gating](#capability-gating).
+
+
+ Revealing secret-like label values requires an admin role. If you are signed in as an admin and still do not see **Reveal**, the label may not be classified as redacted (or already has been revealed for this view). See [Sensitivity and redaction](#sensitivity-and-redaction).
+
+
+ This means `docker compose config` failed for the stack, the same render step [Compose Doctor](/features/compose-doctor) depends on. While unresolved, the **Declared in Compose** section is empty and reconciliation hints do not appear; runtime data is still shown, with **Unknown** provenance. Fix the underlying Compose or environment problem (check Compose Doctor's report on the same stack) and reopen the tab.
+
+
+ Sencho could not inspect that specific replica, so its runtime labels and reconciliation hints are skipped rather than shown as false gaps. The rest of the stack's services and replicas are unaffected. Confirm the container still exists and the node's Docker daemon is reachable, then reopen the tab.
+
+
+
+## Related features
+
+
+
+ A different, editable kind of label: per-node tags used to group stacks in the sidebar, unrelated to Docker/Compose labels.
+
+
+ Renders the same effective Compose model to check for deploy-blocking problems, separate from label reconciliation.
+
+
+ Uses the same key-name heuristic to flag likely-secret environment variables without ever exposing a value.
+
+
+ How capability gating works across a fleet with mixed Sencho versions.
+
+
diff --git a/docs/features/editor.mdx b/docs/features/editor.mdx
index e62b9470..28c7bdf4 100644
--- a/docs/features/editor.mdx
+++ b/docs/features/editor.mdx
@@ -27,6 +27,10 @@ The top card on the left holds the stack's identity and primary controls.
The action bar runs every state transition for the whole stack. The primary buttons (**Start**, **Restart**, **Stop**, **Take down** when running, **Update**) require the `stack:deploy` permission; the **Delete** entry in the kebab dropdown requires the `stack:delete` permission. The bar still appears when only **Delete** is authorised so the operator has a way to remove the stack.
+
+ If a stack is the Sencho instance you are currently signed into, its deploy/delete actions are protected: clicking any of them opens a **Sencho instance protected** dialog instead of running the action. Update Sencho from **Fleet → Node Update**, or move Sencho's own compose project outside `COMPOSE_DIR` to manage it as a normal stack.
+
+
| Button | Behavior |
|--------|----------|
| **Start** / **Restart** | A single button that becomes **Restart** when at least one container is running and **Start** otherwise. |
@@ -63,6 +67,13 @@ Below the action bar, the **CONTAINERS** section lists every container the stack
When the container has a compose service name attached, an extra `⋮` button appears at the right of the row. It opens **Restart service**, **Stop service**, or **Start service** depending on the current state. Service actions affect a single service; for full-stack operations use the action bar above.
+### Multi-container stacks
+
+Stacks with more than one container gain a summary strip above the container list: a total count plus `up`, `paused`, and `unhealthy` counts (only the non-zero ones show). Two view controls sit beside it:
+
+- **Compact / Detailed** toggles whether the CPU/Mem/Net sparkline grid renders under every container row. Detailed is the default for a single-container stack; multi-container stacks default to Compact.
+- **Expand containers** collapses the Logs pane so the container list fills the whole left column. Expanding containers and expanding logs (below) are mutually exclusive; opening one closes the other.
+
## Anatomy panel
The right column shows the **Anatomy panel** by default: a read-only summary of the compose file alongside a scrollable tab row for other stack views.
@@ -99,6 +110,12 @@ The Anatomy tab lists:
When an image update is available, or the stack declares services with a local `build:` section, an inline banner appears at the top of the panel. Registry updates follow version-bump severity: `safe to apply` (patch), `review recommended` (minor), `breaking changes possible` (major), or `review required` when the bump cannot be classified. Build-only stacks show **Rebuild available** with a **Rebuild & Update** button. The banner runs the same operation as the action bar's **Update**; it is hidden for roles that lack the `stack:edit` permission and when the bump is flagged as blocked.
+If the most recent post-deploy vulnerability scan for this stack failed, partially failed, or was skipped, a dismissible amber banner appears below the update banner (`Last post-deploy scan failed.`, `...partially failed.`, or `Post-deploy scan did not run.`). See [Vulnerability Scanning](/features/vulnerability-scanning) for what triggers a post-deploy scan.
+
+
+
+
+
## 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
The editor blocks a silent loss of in-progress edits. Click **Cancel** to return to the original node with your edits intact; click **Discard** to abandon them and proceed to the other node.
+
+ That stack is running the Sencho instance you are signed into, and its deploy/delete actions are blocked to prevent locking yourself out. Update Sencho from **Fleet → Node Update** instead. To manage the stack normally, move its compose project to a directory outside `COMPOSE_DIR`.
+
The stats WebSocket failed to open, or closed unexpectedly. This usually means the Docker daemon on the node is unreachable. Container status, action buttons, and logs continue to work; only live CPU / memory / network rates pause.
diff --git a/docs/features/environment-guardrails.mdx b/docs/features/environment-guardrails.mdx
index 2fa2999c..dee27a28 100644
--- a/docs/features/environment-guardrails.mdx
+++ b/docs/features/environment-guardrails.mdx
@@ -9,7 +9,7 @@ The **Environment** tab in the right-hand **Anatomy** panel answers a question C
The inventory is advisory and read only. It never changes a stack, and it works entirely from variable **names**: a value is never read into the report, the checklist, or the logs, so nothing sensitive is exposed.
-
+
## 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.
-
+
-Fleet Actions runs admin-only. Operator and viewer roles see the tab but cannot apply the cards.
+Fleet Actions runs admin-only. Operator and viewer roles see the tab and every card, but cannot apply them.
## What Fleet Actions covers (and what it doesn't)
@@ -26,77 +26,93 @@ Fleet Actions is the home for operations that span the fleet but don't fit anywh
| Trigger a Sencho self-update across remote nodes | **Check Updates** button on the Fleet masthead |
| Steer where new blueprint deployments land | [Fleet Federation](/features/fleet-federation) |
| Replicate scan policies and CVE suppressions to remotes | [Fleet Sync](/features/fleet-sync) |
+| Reclaim disk space on a single node with an itemized, undo-safe preview | **Resources → Prune** on that node (a different endpoint and flow from the fleet-wide card; see [Prune Docker resources fleet-wide](#prune-docker-resources-fleet-wide)) |
-## Three cards, three execution paths
+## How every card works: preview, confirm, execute
-The cards share a tab and a role gate, but they don't share an execution path. Knowing which path runs explains the result panels and the failure modes.
+All three cards share one execution model, and understanding it explains every result panel, timeout, and edge case below.
-| Card | Endpoint | Where it runs | Scope |
-|---|---|---|---|
-| Stop fleet by label | `POST /api/fleet/labels/fleet-stop` | Control instance orchestrates; fans out to each node | Every configured node |
-| Bulk label assign | `POST /api/fleet/labels/bulk-assign` | Control instance orchestrates; fans out to each target node | The stacks you select across nodes |
-| Prune Docker resources fleet-wide | `POST /api/fleet/labels/fleet-prune` | Control instance orchestrates; fans out to each node | Every reachable node |
+1. **You describe the target.** A stack label name (Stop by label), a label plus checked stacks (Bulk label assign), or a set of resource targets and a scope (Prune).
+2. **A live, debounced readout resolves the real blast radius.** Typing a label name or checking a target fires a non-destructive preview call (`POST /api/fleet/labels/match-preview` or `POST /api/fleet/prune/estimate`) roughly 350-500ms after you stop changing input. The readout in the card's toolbar shows `awaiting target` until something is selected, `resolving…` while the call is in flight, and then a concrete count (`7 stacks · 1 nodes`, `~ 6.09 GB reclaimable`). The primary button stays disabled until this resolves to a non-zero, non-loading result.
+3. **You confirm against the resolved list, not the input.** Clicking the primary button opens a confirmation dialog that lists the actual nodes and stacks (Stop, Bulk assign) or restates the scope (Prune). For Stop by label specifically, the confirmation carries the exact node/stack list the preview resolved, and the real stop only touches stacks that are still in that list *and* still carry the label at execution time: a stack that gains the label after you opened the confirmation is never touched, and a node that reconnects after the preview does not get pulled into the stop.
+4. **The control instance fans the confirmed action out to every node in parallel.** The local node runs in-process; each remote node is called over the standard Bearer-token proxy path. A node that cannot be reached, returns a non-2xx response, or returns a shape Sencho does not recognize is reported as a failure for that node only; the fan-out to every other node still completes.
+5. **Results render per node, grouped and expandable**, in a `Per-node breakdown` section below the form. Stop and Prune also expose a **Dry run** button that walks the identical code path and locks without performing the destructive step, so you can rehearse the exact fan-out before committing.
-All three cards orchestrate from the control instance and report results grouped by node. Stop and Prune iterate every node in **Settings → Nodes**; Bulk label assign iterates only the nodes whose stacks you selected. Each card runs the authoritative work on the executing node (the local node in process, every remote over the node proxy), so unreachable nodes show up in the results with a transport error rather than blocking the rest of the batch.
+## The three cards
-## Stop fleet by label
+| Card | Class | Destructive endpoint | Preview / estimate endpoint | Scope |
+|---|---|---|---|---|
+| Stop by label | Destructive | `POST /api/fleet/labels/fleet-stop` | `POST /api/fleet/labels/match-preview`, `GET /api/fleet/labels/suggestions` | Every configured node |
+| Bulk label assign | Transformative | `POST /api/fleet/labels/bulk-assign` | (computed client-side from `/api/labels` and `/api/fleet/node/:id/stacks` per node) | Only the nodes whose stacks you select |
+| Prune Docker resources fleet-wide | Maintenance | `POST /api/fleet/labels/fleet-prune` | `POST /api/fleet/prune/estimate` | Every configured node |
-Stop every stack assigned a given **stack label** on every node where that stack label exists. Stack labels are matched **by name** across the fleet, so a stack label called `production` on one node and an independently-authored `production` label on another node both match. This action targets stack labels only; node labels (used for node grouping) are never used here. See [Stack Labels](/features/stack-labels) for how to author the selector taxonomy.
+Every card is admin-only and available on every license tier. Stop and Prune iterate every node in **Settings → Nodes**; Bulk label assign iterates only the nodes whose stacks you actually checked. Each card runs the authoritative work on the executing node (the local node in process, every remote over the node proxy), so an unreachable node shows up in the results with a transport error rather than blocking the rest of the batch.
+
+## Stop by label
+
+Stop every stack assigned a given **stack label** on every node where that stack label exists. Stack labels are matched **by name, case-sensitively,** across the fleet, so a stack label called `Media` on one node and an independently-authored `Media` label on another node both match, while `media` does not. This action targets stack labels only; node labels (used for node grouping) are never used here. See [Stack Labels](/features/stack-labels) for how to author the selector taxonomy.
### Step by step
1. Open **Fleet → Actions**.
-2. Type a name in the **Stack label** field. The picker queries each reachable node for its own stack labels and lists each name once with its combined stack and node counts (and the carrying node names), so the scope is unmistakable. A stack label that exists only on a remote node still appears. When a node cannot be reached, the picker notes that the suggestions may be incomplete. You can also type a name by hand.
-3. As you type, a live preview resolves the blast radius across the fleet and the card lists which nodes and stacks match. **Stop fleet** stays disabled until that preview resolves to at least one matching stack, so the action can never run before you can see what it will hit. If the live preview is unavailable, **Dry run** rehearses the same fan-out without stopping anything and resolves the blast radius the same way.
-4. Click **Stop fleet**.
-5. A confirmation appears with the kicker **Fleet stop**, the title `Stop all stacks with the stack label ""?`, and the resolved list of nodes and stacks that will be stopped. Review it, then click **Stop fleet** to commit.
+2. Type a name in the **Stack label** field. The autocomplete queries every reachable node for its own stack labels (`GET /api/fleet/labels/suggestions`) and lists each name once with its combined stack and node counts. A stack label that exists only on a remote node still appears. When a node cannot be reached, a note under the field warns that suggestions may be incomplete. You can also type a name that isn't suggested; the live preview and the eventual stop still resolve it authoritatively against every node.
+3. As you type, the **Preview** section resolves the live blast radius: which nodes carry the label, how many stacks match on each, and which nodes could not be reached. The toolbar readout mirrors this (`7 stacks · 1 nodes`), and **Stop fleet** stays disabled until it resolves to at least one matching stack.
+4. Click **Stop fleet**. A confirmation dialog opens with the kicker **Fleet stop**, the title `Stop all stacks with the stack label ""?`, and the resolved node/stack list ("Will stop 7 stacks across 1 node. Only these stacks are stopped, and only while they still carry the label.").
-
+
+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 ` (no matching stack label)` and is counted in the **failed** badge. This is not a transport failure, it just means the stack label was not present on that node.
+- A node that has no stack label by that name appears as ` (no matching stack label)` and is counted in the **failed** badge. This is not a transport failure; it just means the label was not present on that node.
- A node where the label exists but no stacks are assigned to it appears with a matched count of zero stacks. No per-stack rows render.
-- When the control instance reaches a remote node, the per-stack results you see come from the remote node's own response. A remote the control cannot reach, or that returns a non-2xx, is reported once as a single ` (unreachable)` row carrying the reason, not as a per-stack error, and it never blocks the stops on the reachable nodes.
+- A remote the control cannot reach, or that returns a non-2xx or malformed response, is reported once as a single ` (unreachable)` row carrying the reason, not as a per-stack error, and it never blocks the stops on the reachable nodes.
+
+### Confirmed-target binding and mixed-version fleets
+
+The real stop (not the dry run) sends the confirmed node/stack list from the preview along with the label name. The backend intersects that list against the label's current, live assignment before stopping anything, so drift between the moment you saw the preview and the moment you clicked confirm cannot widen the blast radius in either direction: a stack that gained the label after the preview is never stopped, and a node that reconnects between preview and confirm is not pulled in.
+
+This binding requires the remote node to understand the confirmed-target contract. If a remote is running an older Sencho version that does not advertise support for it, a real stop bound to specific stacks refuses to send to that node and reports it as needing an upgrade rather than silently falling back to stopping everything the label matches there. Dry runs carry no binding and are unaffected; they preview safely against any remote version.
### Behaviour and partial-failure semantics
-- The endpoint always returns 200 with a `results` array. Partial failures live inside that array; the HTTP status is not the place to look.
-- Each remote node call carries a 60-second timeout. A slow remote with many stacks can produce a clean per-stack list or a timeout row, depending on whether the remote streamed before the timeout fired.
-- Every node, local or remote, runs the stop under a per-node bulk-action lock shared with the per-label action endpoint, so a fleet stop and a per-label stop aimed at the same node serialize cleanly instead of double-stopping the same containers. On a remote, the lock is held on that remote while it runs its share of the stop.
+- The endpoint always returns `200` with a `results` array. Partial failures live inside that array; the HTTP status is not the place to look.
+- Each remote node call carries a 60-second timeout. A slow remote with many stacks can produce a clean per-stack list or a timeout row, depending on whether the remote streamed a response before the timeout fired.
+- Every node, local or remote, runs the stop under a per-node bulk-action lock shared with the per-label action endpoint (`POST /api/labels/:id/action`), so a fleet stop and a per-label stop aimed at the same node serialize cleanly instead of double-stopping the same containers.
## Bulk label assign
-Add a stack label to stacks across the fleet in one round trip. Pick a label that exists anywhere in the fleet, select stacks on one or more nodes, and Sencho assigns that label on each target node, creating it there first if the node does not have it yet. Existing labels on the selected stacks are preserved. Use this to keep a label like `media` consistent across nodes without visiting each one to recreate and assign it by hand.
+Add a stack label to stacks across the fleet in one round trip. Pick a label that exists anywhere in the fleet, check stacks on one or more nodes, and Sencho assigns that label on each target node, creating it there first if the node does not already have it. Existing labels on the selected stacks are preserved. Use this to keep a label like `Media` consistent across nodes without visiting each one to recreate and assign it by hand.
-
+
### 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:`). A second prune launched against the same local node while the first is still in flight returns *A prune is already running on this node* for each target.
-- Reclaimed bytes are reported by the Docker daemon and are approximate. Per-node rows in the results panel sum the per-target reclaim; the per-target children show how much each prune actually freed.
+- Changing a target or the scope re-triggers a debounced call to `POST /api/fleet/prune/estimate`, which walks the same Docker enumeration the destructive path uses so the estimate matches what pruning would actually reclaim. **Prune fleet** stays disabled until the estimate resolves: you cannot confirm a destructive fleet-wide prune with no context on what it will reclaim.
+- Each remote node receives one `POST /api/system/prune/system` call per selected target, with a 120-second timeout. If a transport error fires for one target, the remaining targets on that node are short-circuited with the same error rather than retried, so a dead node doesn't absorb the full multi-target timeout budget.
+- Local nodes serialize against a per-node lock (`bulk-prune:`). A second fleet prune launched against the same local node while the first is still in flight returns *A prune is already running on this node* for each target.
+- Reclaimed bytes are reported by the Docker daemon and are approximate. Per-node rows in the results panel sum the per-target reclaim; the per-target children show how much each individual prune actually freed.
+
+
+Fleet Actions' prune card calls the same node-local prune route as the single-node **Resources → Prune** page, but without that page's itemized plan-and-fingerprint flow. It never returns the `PRUNE_PLAN_STALE` (409) error you can see on Resources; each fleet prune call targets exactly one resource type per node and executes immediately. If you want an itemized, reviewable plan before pruning a specific node, use that node's own Resources page instead.
+
## Prerequisites
| Requirement | Why it matters |
|---|---|
-| **Configured remote nodes in Settings → Nodes** | The two fan-out cards iterate the configured node list. A node missing its `api_url` or `api_token`, or one that cannot be reached, is reported once per node as unreachable (Stop fleet by label) or per target (Prune), and never blocks the reachable nodes. |
-| **Labels you intend to target** | Stop fleet by label and the autocomplete depend on labels existing on at least one node. See [Stack Labels](/features/stack-labels) for the authoring flow. |
+| **Configured remote nodes in Settings → Nodes** | Stop and Prune iterate the configured node list; Bulk label assign iterates whichever nodes you select stacks on. A node missing its `api_url` or `api_token`, or one that cannot be reached, is reported once per node as unreachable and never blocks the reachable nodes. |
+| **Admin role** | Every card requires the admin role to apply. |
+| **Labels you intend to target** | Stop by label and its autocomplete depend on stack labels existing on at least one node; Bulk label assign depends on at least one stack label existing anywhere in the fleet. See [Stack Labels](/features/stack-labels) for the authoring flow. |
## Behaviour and lifecycle
-- **Always returns 200.** Every card is structured so that the HTTP status reflects the request shape, not the operational outcome. Partial failure is encoded in per-row fields, not in the status code.
+- **Always returns 200.** Every destructive endpoint is structured so the HTTP status reflects the request shape, not the operational outcome. Partial failure is encoded in per-row fields, not in the status code.
- **No retry, no scheduling, no undo.** Fleet Actions runs synchronously and is operator-driven; there is no background scheduler and no roll-back. For recurrence, use [Scheduled Operations](/features/scheduled-operations).
- **Offline remotes still receive the request.** A node that is down at the moment of the action returns a transport-error row but does not block the fan-out across the rest of the fleet.
-- **Concurrent runs serialize per node.** Both fan-out cards take per-node locks before hitting Docker, so kicking off a second prune (or a second fleet stop and a per-label stop) while the first is in flight yields a calm *A bulk action is already running on this node* / *A prune is already running on this node* row rather than silent overlap.
+- **Concurrent runs serialize per node.** All three cards take per-node locks before touching Docker or the label tables, so kicking off a second prune, a second fleet stop, or a fleet stop overlapping a per-label stop on the same node yields a calm "already running on this node" row rather than silent double-execution.
## Limitations and non-goals
-Fleet Actions is intentionally narrow in v1. The following are deliberately out of scope:
+Fleet Actions is intentionally narrow. The following are deliberately out of scope:
- **No replace mode for bulk label assign.** The card only adds the label you pick; it never removes a stack's existing labels. To remove or swap labels on a stack, edit them from that stack's own view.
- **No fleet-wide bulk start, restart, or update.** Stop is the only fleet-wide stack lifecycle action today. Per-node multi-stack start, restart, and update live in the sidebar's Bulk mode.
-- **No label-set selectors.** Stop fleet by label matches one label name. Combinations like "stacks labelled A AND B" are not supported.
-- **No partial-stop ceiling.** The Stop card stops every stack the label matches; there is no "stop the first N" knob.
+- **No label-set selectors.** Stop by label matches one label name. Combinations like "stacks labelled A AND B" are not supported.
+- **No partial-stop ceiling.** The Stop card stops every stack the resolved blast radius contains; there is no "stop the first N" knob.
- **No undo.** A stopped stack stays stopped until you start it again; a pruned image is gone until it is pulled or rebuilt.
- **Approximate reclaim numbers.** The bytes the Prune card reports come from the Docker daemon and are best-effort, not authoritative.
-- **60-second remote timeout on fleet-stop, 120-second remote timeout per prune target.** A remote with many stacks or a very slow filesystem may produce timeout rows before the work fully completes. The action itself usually still finishes on the remote; the control instance just stopped waiting.
+- **Confirmed-target stops need a current remote.** A real (non-dry-run) stop bound to specific stacks refuses to run against a remote that doesn't advertise support for confirmed-target binding; upgrade the remote to retry.
+- **Timeouts scale with the fan-out, not with any one node.** 60 seconds per remote on fleet-stop and bulk-assign, 120 seconds per remote per prune target. A remote with many stacks or a very slow filesystem may produce a timeout row before the underlying work fully completes; the action itself usually still finishes on the remote, the control instance just stopped waiting.
## Practical workflows
### Pause everything before a power event
-Tag the stacks you want to bring down with a dedicated label (for example `evening-shutdown`). Run **Stop fleet by label** with that label name. The per-node breakdown confirms each stack's stop result; restart from the sidebar when power is back.
+Tag the stacks you want to bring down with a dedicated label (for example `evening-shutdown`). Run **Stop by label** with that name and review the live preview before confirming. The per-node breakdown confirms each stack's stop result; restart from the sidebar when power is back.
+
+### Rehearse a destructive action before committing
+
+For Stop and Prune, click **Dry run** first. It walks the identical lock, fan-out, and per-node logic as the real action but skips the destructive leaf call, so the results panel shows exactly what would happen (including which nodes are unreachable right now) before you commit to it.
### Propagate a label across the fleet
-Define a label like `media` on one node (for example the local node) under **Settings → Labels**, then open **Bulk label assign**, pick `media` under the label source, and check the matching stacks on every node. Sencho creates `media` on each node that does not have it and assigns it, leaving each stack's other labels intact. The label then matches fleet-wide in **Stop fleet by label** and in per-node stack filtering.
+Define a label like `Media` on one node (for example the local node) under **Settings → Labels**, then open **Bulk label assign**, pick `Media` under the label source, and check the matching stacks on every node. Sencho creates `Media` on each node that does not have it and assigns it, leaving each stack's other labels intact. The label then matches fleet-wide in **Stop by label** and in per-node stack filtering.
### Free disk before a heavy deploy
-Run **Prune Docker resources fleet-wide** with **Images** selected and **Managed only** scope. The reclaim shows up per node and gives you a quick read on which hosts had accumulated the most stale layers. Switch to **All unused** if you want the prune to reach workloads that Sencho does not manage.
+Run **Prune Docker resources fleet-wide** with **Images** selected and **Managed only** scope, and check the live per-node estimate before confirming. It gives a quick read on which hosts have accumulated the most stale layers. Switch to **All unused** if you want the prune to reach workloads that Sencho does not manage.
+
+## Common questions
+
+
+
+ Bulk mode operates on a hand-picked set of stacks **on one node** and supports start, stop, restart, and update. Fleet Actions operates **across every configured node** by selector (a label, or a checked cross-node set), and only Stop is a lifecycle action here (Bulk mode covers restart and update, Fleet Actions does not).
+
+
+ No. Dry run walks the same code path, including acquiring the per-node lock, but every card skips the destructive Docker or label-table call and returns what it would have done instead. It is safe to run repeatedly.
+
+
+ Every destructive or state-changing button stays disabled until the live preview or estimate resolves to a non-zero, non-loading result. This is deliberate: you always confirm against a concrete, current blast radius rather than an unresolved input.
+
+
+ Resources → Prune builds an itemized plan with a fingerprint and re-validates it at execute time, which is where that error comes from. Fleet Actions' prune card calls the simpler legacy single-target path on each node instead, so there is no plan to go stale.
+
+
## Troubleshooting
- The fleet-stop match is by **stack label name**, not label ID. Confirm the stack label name on the affected node under **Settings → Labels**; a typo, a case mismatch, or a trailing space will leave the node out. Stack labels are scoped per node, so renaming the label on one node does not propagate to the others.
+ The fleet-stop match is by **stack label name, case-sensitively**, not label ID. Confirm the exact stack label name on the affected node under **Settings → Labels**; a typo, a case mismatch (`media` vs `Media`), or a trailing space will leave the node out. Stack labels are scoped per node, so renaming the label on one node does not propagate to the others.
- The picker queries each reachable node for its own stack labels, so a name is missing when no reachable node has a **stack label** by that name, or when the node that owns it could not be reached when the picker loaded (the picker flags this case). Node labels never appear here, because this action targets stack labels only. You can always type a name by hand; the fleet-stop request still iterates every node and asks each one authoritatively.
+ The picker queries each reachable node for its own stack labels, so a name is missing when no reachable node has a **stack label** by that name, or when the node that owns it could not be reached when the picker loaded (a note under the field flags this case). Node labels never appear here, because this action targets stack labels only. You can always type a name by hand; the live preview and the fleet-stop request still resolve it authoritatively against every node.
- Stack names must match the standard validator: alphanumeric plus dash and underscore, no spaces, no path separators. A name that does not exist on its node returns *Stack not found*. Each stack is validated independently, so one bad row does not block the rest of the batch. Fix the offending entry and re-run; the rows that already succeeded are simply re-added with no change.
+ Stack names must match the standard validator: alphanumeric plus dash and underscore, no spaces, no path separators. A name that does not exist on its node returns *Stack not found*. Each stack is validated independently, so one bad row does not block the rest of the batch.
Apply needs both a label picked under the source and at least one stack checked. An empty label source means no stack labels are defined on any reachable node yet; create one under **Settings → Labels** first. Wait for the per-node lists to finish loading before selecting.
-
- Each remote node call carries a 60-second timeout. A remote with many stacks or a slow Docker daemon can outlast that budget. Check the affected remote's logs; the stop usually completed on the remote even though the control instance stopped waiting. Re-running the same fleet-stop is safe: stacks that are already stopped return the per-stack error *No containers found for this stack* and do not toggle anything else.
+
+ That remote is running a Sencho version that doesn't advertise support for confirmed-target binding, the mechanism that binds the real stop to exactly the stacks you saw in the preview. Upgrade the remote node and retry; Dry run is unaffected and works against any version.
+
+
+ Each remote node call carries a 60-second timeout. A remote with many stacks or a slow Docker daemon can outlast that budget. Check the affected remote's logs; the action usually completed on the remote even though the control instance stopped waiting. Re-running is safe for Stop: stacks that are already stopped return the per-stack error *No containers found for this stack* and do not toggle anything else.
- The per-node prune lock is held while the first prune is in flight. Wait for the first run to finish or click **Refresh** on the Fleet masthead to confirm it has cleared, then re-run. The lock is released automatically when the first run exits.
+ The per-node prune lock is held while the first prune is in flight. Wait for the first run to finish, then re-run. The lock is released automatically when the first run exits.
-
- Fleet Actions runs admin-only. Confirm the active user has the admin role under **Settings → Users**. Operator and viewer roles see the cards but cannot apply them.
+
+ Fleet Actions runs admin-only. Confirm the active user has the admin role under **Settings → Users**; operator and viewer roles see every card but cannot apply them.
-
- The node is in **Settings → Nodes** but its `api_url` or `api_token` is missing, expired, or unreachable. Stop fleet by label reports it once as a single ` (unreachable)` row; Prune reports it per target. Open **Settings → Nodes** on the control instance and click **Test connection** for the remote; fix the credential or the reachability, then re-run the action.
+
+ The node is in **Settings → Nodes** but its `api_url` or `api_token` is missing, expired, or unreachable. Stop by label reports it once as a single ` (unreachable)` row; Prune reports it per target. Open **Settings → Nodes** on the control instance and test the connection for the remote; fix the credential or the reachability, then re-run the action.
@@ -200,13 +247,29 @@ Run **Prune Docker resources fleet-wide** with **Images** selected and **Managed
Fleet Actions is one slice of the Fleet view. Each adjacent surface answers a different question.
-| Related feature | What it covers | Why it is not Fleet Actions |
-|---|---|---|
-| [Stack Labels](/features/stack-labels) | Author the per-node label taxonomy that Fleet Actions targets. | Fleet Actions consumes labels; Stack Labels create and rename them. |
-| [Stack Sidebar · Bulk mode](/features/sidebar#bulk-mode) | Per-node, per-stack bulk start, stop, restart, and update. | Bulk mode handles a hand-picked subset of stacks on one node; Fleet Actions runs by selector across many nodes. |
-| [Scheduled Operations](/features/scheduled-operations) | Recurring or one-shot scheduled stack operations. | Scheduled Operations owns recurrence; Fleet Actions is always operator-initiated. |
-| [Fleet Federation](/features/fleet-federation) | Operator-driven placement controls (cordon, pin) for [Blueprints](/features/blueprint-model). | Federation steers declarative placement; Fleet Actions runs imperative operations on what already exists. |
-| [Fleet Sync](/features/fleet-sync) | Push-only replication of security rules from a control instance to its replicas. | Fleet Sync replicates state; Fleet Actions runs operations. |
-| [Multi-Node Management](/features/multi-node) | How nodes get added to the fleet (proxy or pilot mode) and how the license tier propagates. | Multi-Node Management is the prerequisite; Fleet Actions runs against whatever Multi-Node Management already configured. |
-| [Fleet View](/features/fleet-view) | The masthead, tab strip, and node grid that host the Actions tab. | Fleet Actions is one tab inside Fleet View. |
-| [Licensing](/features/licensing) | Community and Admiral plans. | Licensing covers plan coverage across the product; Fleet Actions only cites the admin role check above. |
+
+
+ Author the per-node label taxonomy that Fleet Actions targets. Fleet Actions consumes labels; Stack Labels create and rename them.
+
+
+ Per-node, per-stack bulk start, stop, restart, and update. Handles a hand-picked subset of stacks on one node; Fleet Actions runs by selector across many nodes.
+
+
+ Recurring or one-shot scheduled stack operations. Owns recurrence; Fleet Actions is always operator-initiated.
+
+
+ Operator-driven placement controls for Blueprints. Steers declarative placement; Fleet Actions runs imperative operations on what already exists.
+
+
+ Push-only replication of security rules from a control instance to its replicas. Replicates state; Fleet Actions runs operations.
+
+
+ How nodes get added to the fleet and how license tier propagates. The prerequisite; Fleet Actions runs against whatever Multi-Node Management already configured.
+
+
+ The masthead, tab strip, and node grid that host the Actions tab.
+
+
+ Community and Admiral plans. Fleet Actions is available on every tier; only the admin role check applies.
+
+
diff --git a/docs/features/fleet-backups.mdx b/docs/features/fleet-backups.mdx
index 5e538bf3..bd8a5c17 100644
--- a/docs/features/fleet-backups.mdx
+++ b/docs/features/fleet-backups.mdx
@@ -18,7 +18,7 @@ Create point-in-time snapshots of every `compose.yaml` and `.env` file across yo
4. Click **Create** to capture files from every reachable node
-
+
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: ``" description, alongside manual and scheduled ones.
+
+The [Storage Portability](/features/compose-storage) tab surfaces the same coverage for any stack with persistent volumes: a warning card and a **Take a fleet snapshot →** link appear when the stack has no fleet snapshot in the last 7 days, or the relative time since the last one otherwise.
### Capturing stack documentation
@@ -47,8 +53,10 @@ The snapshot list shows each snapshot in a table with the following columns:
- **Warnings** - a warning icon with a count if any nodes or stacks were skipped, or "None"
- **Actions** - **View** to open the detail view, a cloud-upload icon for snapshots not yet mirrored to a configured Recovery Vault or custom off-site target, and a delete button for admins
+The list paginates at 10 snapshots per page; use the arrows next to **Create Snapshot** to page through history.
+
-
+
## 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 `-`, for example `web-compose.yaml`).
-
+
-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.
+
+## Where Fleet Backups fits
+
+Fleet snapshots show up in several other views. Each adjacent surface answers a different question.
+
+| Related feature | What it covers | Why it is not Fleet Backups |
+|---|---|---|
+| [Health-Gated Updates](/features/health-gated-updates) | The readiness check before a stack update or rebuild. | Health-Gated Updates gates the update itself; Fleet Backups is the point-in-time capture it can optionally trigger first. |
+| [Storage Portability](/features/compose-storage) | Per-stack mount inventory and portability verdict. | Storage Portability surfaces snapshot recency for stacks with persistent volumes; Fleet Backups owns the actual capture and restore. |
+| [Scheduled Operations](/features/scheduled-operations) | Recurring or one-shot scheduled stack operations. | Scheduled Operations owns recurrence and dispatch; Fleet Backups defines what a snapshot captures and how to restore it. |
+| [Stack Dossier](/features/stack-dossier) | Operator-authored notes about a stack (purpose, owner, access, recovery). | Stack Dossier owns the notes; Fleet Backups only preserves a read-only copy of them inside a snapshot when documentation capture is on. |
+| [Atomic Deployments](/features/atomic-deployments) | The rollback-point mechanism created on every local deploy. | Atomic Deployments backs up automatically on every deploy; Fleet Backups is a separate, on-demand, fleet-wide capture you trigger yourself. |
+| [Multi-Node Management](/features/multi-node) | How nodes join the fleet and which top-level views are hub-only. | Multi-Node Management is why Snapshots and Recovery Vault are hub-only; Fleet Backups just inherits that rule. |
+| [Licensing](/features/licensing) | Community and Admiral plans. | Licensing covers plan coverage across the product; Fleet Backups only cites the Recovery Vault tier requirement above. |
diff --git a/docs/features/fleet-dossier.mdx b/docs/features/fleet-dossier.mdx
index cb55920d..2abae24a 100644
--- a/docs/features/fleet-dossier.mdx
+++ b/docs/features/fleet-dossier.mdx
@@ -25,12 +25,14 @@ homelab-dossier/
- **`index.md`** lists every node with its type, reachability, and stack count, plus an explicit section for any node that was skipped and why.
- **`network.md`** aggregates across the fleet: a **port map**, **volume map**, and **network map**, an **environment checklist** by stack, an **access-URL inventory**, and a **VLAN / static IP / firewall** table. The last two are populated from your Stack Dossier notes.
- **`nodes/.md`** summarises a node and links to its stacks.
-- **`stacks/--.md`** is the same combined document the Stack Dossier's own export produces: generated anatomy followed by your operator notes.
+- **`stacks/--.md`** combines that stack's generated anatomy with your operator notes, in the same format as the Stack Dossier's own export (see below for the one section it omits).
## Generated facts and your notes
Everything Sencho can derive (services, published ports, volumes, networks, restart policy, env file and its variable count, missing `${VAR}` references, and Git source) is generated live from each stack's current Compose file, so it never drifts. Everything Sencho cannot infer (purpose, owner, access URLs, static IP, VLAN, firewall, reverse proxy, backup, upgrade, and recovery notes) comes from the Stack Dossier you maintain per stack. Stacks without notes are still included, documented from their generated facts alone.
+When the Networking tab capability is available on a node, each of that node's stack pages also includes a network exposure summary (stack-level exposure intent and per-service port reachability), the same section the [Stack Dossier](/features/stack-dossier) export shows. The Fleet Dossier does not include the Storage tab's storage summary, even when that capability is available and a stack's own Stack Dossier export would show it; download that stack's individual dossier if you need its storage summary.
+
## Unreachable nodes
A remote node that is offline or unreachable when you export is never allowed to block the rest of the fleet. It appears in `index.md` under **Skipped nodes** with the reason, gets its own node page marked unreachable, and contributes no stack pages. Everything reachable still exports normally.
@@ -44,9 +46,13 @@ As with the Stack Dossier, the generated facts only ever carry env variable **na
Administrators can export the fleet dossier:
1. Open the **Fleet** view.
-2. Click **Export Dossier** in the toolbar.
+2. Click **Export Dossier** in the toolbar, next to Refresh.
3. Your browser downloads `homelab-dossier.zip`. Unzip it wherever you keep documentation.
+
+
+
+
## Troubleshooting
@@ -62,4 +68,10 @@ Administrators can export the fleet dossier:
Dossiers and stacks are scoped per node, so a stack with the same name on two nodes exports as two separate pages, each prefixed with its node (for example `local--radarr.md` and `media-node--radarr.md`).
+
+ The Fleet Dossier only ever includes a network exposure summary per stack, never a storage summary, regardless of which capabilities are active on that node. Download the stack's own dossier from its Dossier tab to get the storage summary alongside the same generated facts and notes.
+
+
+ This means the export could not even enumerate your fleet, for example the local node's connection dropped mid-export or your session expired. Unlike a single unreachable remote node, which is skipped and noted in `index.md`, this stops the whole export. Confirm you are still signed in and the local node is reachable, then try again.
+
diff --git a/docs/features/fleet-federation.mdx b/docs/features/fleet-federation.mdx
index 696a37d1..04be6542 100644
--- a/docs/features/fleet-federation.mdx
+++ b/docs/features/fleet-federation.mdx
@@ -3,57 +3,59 @@ title: "Fleet Federation"
description: "Operator-driven placement controls: cordon nodes and pin blueprints to specific nodes."
---
-The **Federation** tab is the placement-control surface for fleets running [Blueprints](/features/blueprint-model). It lets you steer where new deployments land without rewriting selectors or labels: mark a node unschedulable for new work, or force a specific blueprint to remain on a specific node regardless of selector matches. Federation builds on the Blueprints reconciler, so the controls described here only affect blueprint-managed deployments.
+The **Federation** tab is the placement-control surface for fleets running [Blueprints](/features/blueprint-model). It lets you steer where new deployments land without rewriting selectors or labels: mark a node unschedulable for new work, or force a specific blueprint to remain on a specific node regardless of selector matches. Federation builds on the Blueprints reconciler, so the controls described here only affect blueprint-managed deployments, and every placement change they cause still has to pass through the same rollout-confirmation gate as any other Blueprint change.
Federation lives under **Fleet → Federation**.
-
-
+
+
-Federation placement controls (cordon, uncordon, pin, and unpin) are available on every tier. Cordon and pin mutations require an admin user role, or the node-admin role for cordon when scoped to that node.
+Cordon and pin mutations require an admin user role, or the node-admin role for cordon when scoped to that node.
## Placement control, not placement automation
Sencho's blueprint reconciler resolves selectors automatically: when a node grows a matching label, the blueprint deploys; when the label is removed, the blueprint is withdrawn (or `evict_blocked` for stateful workloads). That works well until you need to override the automatic decision: take a node out of rotation for maintenance, keep a stack pinned to one host, or hold ground while you migrate. Federation gives you those overrides as explicit operator actions.
-The model is deliberate: Sencho proposes placements; you confirm or override them. The reconciler never moves an existing deployment in response to cordon or pin. Cordon affects only *new* placements, and pin only changes which node the reconciler considers desired. Eviction from non-pinned nodes still flows through the existing state-review and confirmation prompts.
+The model is deliberate in two layers. First, Sencho proposes placements; you confirm or override them with cordon and pin. The reconciler never moves an existing deployment in response to cordon or pin by itself: cordon affects only *new* placements, and pin only changes which node the reconciler considers desired. Second, changing what the reconciler *wants* to do is not the same as authorizing it to act. Every Blueprint carries a rollout-approval gate ([Blueprints § Apply on demand](/features/blueprint-model#apply-on-demand)): the reconciler only executes place or remove actions that an operator already reviewed and confirmed in a rollout preview. Pinning or unpinning a blueprint always clears that approval, because the pin is part of what Sencho fingerprints as the blueprint's operational intent. Cordoning or uncordoning a node does not touch the blueprint's approval directly, but uncordoning can surface a brand-new placement that was never part of a confirmed plan, and that new placement waits for confirmation the same way a pin change does.
-In practice, Federation hands you four operator decisions:
+In practice, Federation hands you four operator decisions, each of which still ends at a rollout preview before the fleet actually changes:
- **Don't schedule new work here for a while.** Cordon a node.
-- **Anchor this blueprint to one host.** Pin a blueprint.
-- **Hand the decision back to the reconciler.** Uncordon or unpin.
-- **Ride out a migration.** Combine pin (on the destination) with cordon (on the source) so the source drains naturally and the destination becomes the new home.
+- **Anchor this blueprint to one host.** Pin a blueprint, then confirm the rollout.
+- **Hand the decision back to the reconciler.** Uncordon or unpin, then confirm the rollout if it surfaces a new placement.
+- **Ride out a migration.** Combine pin (on the destination) with cordon (on the source) so the source drains naturally and the destination becomes the new home once you confirm.
## Key capabilities
### Cordon a node
-Cordon marks a node as unschedulable. From the moment a node is cordoned, the reconciler skips it for new placements; existing stacks keep running, drift checks keep running, and revision bumps still redeploy in place. The cordon is visible to every tier (the read-only `Cordoned` pill on the node card) so operators can see why a node is not picking up new work. Uncordon to lift the restriction.
+Cordon marks a node as unschedulable. From the moment a node is cordoned, the reconciler stops proposing it for new placements: existing stacks keep running, drift checks keep running, and revision bumps still redeploy in place on that node. Because cordon only *removes* a node from consideration, it needs no separate confirmation; the effect is visible on the node's next reconciliation tick without an Apply now / Confirm Apply round trip. The cordon is visible to every role (the read-only `Cordoned` pill on the node card) so operators can see why a node is not picking up new work. Uncordoning is different: it can make the node newly eligible for a blueprint that targets it, and that eligibility is a real fleet mutation, so it goes through the same rollout preview as any other new placement (see [Behaviour and lifecycle](#behaviour-and-lifecycle) below).
-The cordon reason is free-form text (up to 256 characters). It surfaces in the Federation tab summary, in the cordon pill's tooltip on the node card, and on the audit log row for the action.
+The cordon reason is free-form text (up to 256 characters). It surfaces in the Federation tab summary and in the cordon pill's tooltip on the node card. The action itself is recorded in the audit log with the summary "Cordoned node" or "Uncordoned node."
### Pin a blueprint to a node
Pinning a blueprint forces the reconciler to treat that blueprint as desired only on a single specific node, regardless of what its selector says. The pin overrides the selector entirely. Pin is the right tool when a workload depends on local state, must run on a specific host (a gateway, an integration target), or is in the middle of a host-to-host migration.
-When the target node is removed from the fleet, the pin clears automatically and the blueprint reverts to its selector behaviour on the next reconciliation tick.
+Selecting a node from the **Pinned to** dropdown saves the pin immediately, but pinning also clears the blueprint's rollout approval: the reconciler will not create the deployment on the new target, or remove it from any node the pin no longer covers, until you open that blueprint in **Fleet → Deployments** and confirm a rollout preview. The Federation tab's **Effective** column shows what the reconciler will pursue once authorized, which is not necessarily what is running right now if a confirmation is still pending.
+
+When the target node is removed from the fleet, the pin clears automatically and the blueprint reverts to its selector behaviour, again subject to a fresh confirmation before anything actually redeploys.
### Audit visibility
-Cordon, uncordon, and pin actions all flow through Sencho's standard audit log. Each action records the actor, the affected resource, and the optional reason (for cordon). Filter the **Audit** view by `node.cordon`, `node.uncordon`, or `blueprint.pin` to see the full history.
+Cordon, uncordon, and pin all flow through Sencho's standard audit log, alongside every other mutating request. Each row records the actor, the HTTP method and path, and a short summary: "Cordoned node," "Uncordoned node," or "Updated blueprint pin." There is no separate action-name filter for these events; use the free-text search box on the **Audit** view (it matches against the path and the summary) to find them, for example by searching "cordon" or "pin."
## Prerequisites
| Requirement | Why it matters |
|-------------|----------------|
-| **Community or Admiral on the control instance** | Federation placement is part of the Compose-first control plane. Remote nodes inherit the control instance tier through the [proxy tier assertion](/features/multi-node#license-enforcement-across-nodes). |
| **Admin or node-admin role** | Pin policy edits require admin. Cordon and uncordon require `node:manage` (admin, or node-admin when scoped to that node). Operator and viewer roles can read cordon state but cannot toggle it. |
| **At least one Blueprint defined** | The Pin policy table is empty until you create a blueprint under **Fleet → Deployments**. Cordon does not require any blueprints; it only suppresses *new* placements from blueprints that exist later. |
-| **Active connection to each remote node** | Cordon state is written to the control instance's local database, but it only takes effect once the reconciler runs against the fleet's current node set. A remote node that is `Offline` still carries its cordon flag and resumes honouring it as soon as it comes back. |
+| **A confirmed rollout after any pin change** | Setting or clearing a pin only declares intent. The actual create/remove on the fleet waits for **Apply now → Confirm Apply** on the blueprint's detail sheet, same as any other change to the blueprint's operational intent. See [Blueprints § Apply on demand](/features/blueprint-model#apply-on-demand). |
+| **Active connection to each remote node** | Cordon state is written to the control instance's local database, but it only takes effect once the reconciler runs against the fleet's current node set. A remote node that is `Offline` still carries its cordon flag and resumes honouring it as soon as it comes back. An offline node also blocks Confirm Apply on any pin that targets it, since the rollout preview elevates an unreachable target to a blocker. |
## Step by step
@@ -61,23 +63,19 @@ Cordon, uncordon, and pin actions all flow through Sencho's standard audit log.
Open **Fleet → Overview**. On the card for the node you want to cordon, click the kebab menu (`⋯`) in the top-right corner and choose **Cordon node**.
-
-
-
-
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 . Selector is overridden.`) and the deployment table marks the pinned row with a `Pinned` indicator so the override is obvious anywhere the blueprint surfaces.
+Select a node from the **Pinned to** dropdown to pin. Select *(unpinned)* to clear the pin and hand the decision back to the reconciler. The selection itself saves immediately, a toast confirms it, and the **Effective** column updates in place.
+
+
+
+
+
+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 . Selector is overridden.`) and the deployment table marks the pinned row with a `Pinned` indicator so the override is obvious anywhere the blueprint surfaces.
### Verify
After cordoning or pinning, check the verification surfaces:
-- **Cordon:** the node card carries a `Cordoned` pill in the Fleet → Overview grid, and the node appears in the Federation tab's *Cordoned nodes* section with the timestamp and reason. The audit log records a `node.cordon` event.
-- **Pin:** the Pin policy row's **Effective** column reads the pinned node, the blueprint detail sheet shows the override banner, and the deployment table marks the pinned row. The audit log records a `blueprint.pin` event.
+- **Cordon:** the node card carries a `Cordoned` pill in the Fleet → Overview grid, and the node appears in the Federation tab's *Cordoned nodes* section with the timestamp and reason. The audit log records a "Cordoned node" entry.
+- **Pin:** the Pin policy row's **Effective** column reads the pinned node, the blueprint detail sheet shows the override banner, and the deployment table marks the pinned row once the rollout has been confirmed. Until it is confirmed, the blueprint's catalog tile and detail sheet footer show **pending** or **reapproval required**. The audit log records an "Updated blueprint pin" entry for the pin write itself.
## Behaviour and lifecycle
-| Action | Takes effect | Persists across restart |
-|---|---|---|
-| Cordon | Next reconciliation tick, for new placement and state-review decisions only | Yes |
-| Uncordon | Next reconciliation tick | Yes (the flag is cleared, not just hidden) |
-| Pin | Immediate; the next reconciliation tick treats the pinned node as the only desired target | Yes |
-| Unpin | Immediate; the reconciler reverts to selector behaviour | Yes (cleared) |
-| Pinned node deleted | Pin clears automatically as part of node deletion housekeeping | Yes (cleared) |
+| Action | Takes effect | Requires a confirmed rollout | Persists across restart |
+|---|---|---|---|
+| Cordon | Next reconciliation tick, for new placement and state-review decisions only | No, cordon only removes a candidate placement | Yes |
+| Uncordon | Node becomes newly eligible on the next tick | Yes, if that eligibility produces a new create/update action | Yes (the flag is cleared, not just hidden) |
+| Pin | Saved immediately; clears the blueprint's rollout approval | Yes, always, because the pin is part of the blueprint's fingerprinted intent | Yes |
+| Unpin | Saved immediately; clears the blueprint's rollout approval | Yes, always, for the same reason | Yes (cleared) |
+| Pinned node deleted | Pin clears automatically as part of node deletion housekeeping | Yes, for whatever placement change follows | Yes (cleared) |
The reconciler only re-evaluates cordon when it is deciding where a blueprint should run *next* or whether the live state matches the desired state. Drift checks against existing containers, revision-driven redeploys, and manual deploys initiated from the stack editor all bypass the cordon flag by design. The flag is a hint to the placement layer, not a quarantine on the node.
-The **Effective** column in the Pin policy table is computed live each time the page loads. It is not stored; only the pin itself persists.
+The **Effective** column in the Pin policy table is computed live each time the page loads. It is not stored, and it does not mean "currently running": it shows the node the reconciler will pursue once it has an authorized plan to act on. Only the pin itself, and the blueprint's approval state, persist.
## Security and audit
-Federation mutations require an admin user role, or the node-admin role for cordon when scoped to that node. The `Cordoned` pill on the node card stays visible at every tier as a read-only signal, so operators without placement permissions can still see why a node is skipping new work.
+Federation mutations require an admin user role, or the node-admin role for cordon when scoped to that node. The `Cordoned` pill on the node card stays visible at every role as a read-only signal, so operators without placement permissions can still see why a node is skipping new work.
-Every cordon, uncordon, and pin action is captured in the audit log. Each row carries the actor, the action (`node.cordon`, `node.uncordon`, `blueprint.pin`), the affected resource, and the cordon reason where applicable. Filter the **Audit** view by these action names for the full history.
+Every cordon, uncordon, and pin write is captured in the audit log with the actor, the request path, the status code, and a short summary. There is no dedicated action-name taxonomy for these events (no `node.cordon` or `blueprint.pin` filter); search the **Audit** view's free-text box for "cordon" or "pin" to find the relevant rows, or narrow by the `POST`/`PUT` method filter.
-Federation is hub-only. The cordon flag and the pin live on the control instance, and the blueprint reconciler that reads them also runs on the control instance. Cordoning a remote node does not require any change on the remote itself.
+Federation is hub-only. The cordon flag and the pin live on the control instance, and the blueprint reconciler that reads them also runs on the control instance. Cordoning a remote node does not require any change on the remote itself. The confirm-before-mutate gate described above is a fleet-wide Blueprints property, not something Federation adds on top; Federation's cordon and pin writes are simply two of the inputs that can change what a blueprint's next confirmed rollout will do.
## Limitations and non-goals
@@ -129,45 +147,52 @@ Federation v1 ships the two controls above and nothing more. The following are d
- **Drain node.** Evacuating all blueprints from a node depends on volume migration, which is operator-driven for stateful workloads in the current release.
- **Capacity planning.** Predictive resource utilisation belongs to a later iteration once there is real-world fleet usage data to calibrate against.
-- **Auto-eviction on cordon.** Cordon never withdraws or evicts an existing deployment. Use the deployment table for explicit withdraw or eviction confirmations.
-- **In-place-redeploy block.** Cordon does not stop revision-driven redeploys on the cordoned node. Bumping a blueprint's revision still redeploys the existing stack in place on every node where it already runs, cordoned or not.
+- **Auto-eviction on cordon.** Cordon never withdraws or evicts an existing deployment. Use the deployment table for explicit withdraw or eviction confirmations, which themselves require an authorized remove outcome from a confirmed rollout.
+- **In-place-redeploy block.** Cordon does not stop revision-driven redeploys on the cordoned node. Bumping a blueprint's revision still redeploys the existing stack in place on every node where it already runs, cordoned or not, once that revision change is confirmed.
- **Manual deploy interception.** Cordon affects the blueprint reconciler only. A manual deploy initiated from the stack editor still lands on whichever node you target.
- **Drift-mode override.** Pin does not change the blueprint's drift policy or stateful classification on the pinned node. Both flow through unchanged.
+- **Automatic pin execution.** Pinning or unpinning never deploys or withdraws anything by itself. It only changes what the next rollout preview will propose; you still confirm it.
- **Reason length.** The cordon reason is capped at 256 characters and stored verbatim.
## Practical workflows
### Take a node out of rotation for OS patching
-Cordon the node with a reason that names the work (e.g. *"Patching kernel, back online in 30m"*). The Federation tab summary and the node-card tooltip surface that reason for the rest of your team. Existing stacks keep running while you reboot and patch; new blueprint placements skip the node until you uncordon.
+Cordon the node with a reason that names the work (e.g. *"Patching kernel, back online in 30m"*). The Federation tab summary and the node-card tooltip surface that reason for the rest of your team. Existing stacks keep running while you reboot and patch; new blueprint placements skip the node until you uncordon, and even then nothing deploys onto it until you confirm the resulting rollout.
### Migrate a stack between hosts
-Pin the blueprint to the destination node first. The next reconciliation tick deploys onto the destination. Once health checks pass on the destination, withdraw the original deployment from the source via the deployment table. The pin holds the workload on the destination while you tear down the source, so there is no window where the reconciler can reverse the move.
+Pin the blueprint to the destination node, then open the blueprint in **Fleet → Deployments** and confirm the rollout preview. That single confirmation both authorizes the new placement on the destination and authorizes withdrawing (or, for a stateful blueprint, flagging for eviction) the node the pin no longer covers. Once health checks pass on the destination, withdraw or evict the original deployment from the deployment table if it did not clear automatically. The pin holds the workload on the destination while you tear down the source, so there is no window where the reconciler can reverse the move on its own; the only remaining step is the explicit confirmation you already made.
### Anchor a gateway-only blueprint
-Pin a gateway blueprint (reverse proxy, ingress, tunnel) to the node that owns the public IP. Even if the selector matches a second node, the pin overrides; only the gateway host receives the deployment. If the gateway node is also cordoned for maintenance, the pin wins: the blueprint stays on the pinned node even while it is otherwise unschedulable.
+Pin a gateway blueprint (reverse proxy, ingress, tunnel) to the node that owns the public IP, then confirm the rollout. Even if the selector matches a second node, the pin overrides; only the gateway host receives the deployment. If the gateway node is also cordoned for maintenance, the pin wins: the blueprint stays on the pinned node even while it is otherwise unschedulable.
## Pin overrides cordon
-Cordon governs automatic placement; pin is an explicit operator decision. When the two collide (a blueprint pinned to a cordoned node), pin wins. The blueprint stays on (or deploys onto) the pinned node even though the node is otherwise unschedulable. This keeps cordon's cost predictable: cordoning a node cannot silently break a workload you already chose to anchor there. If you want to remove the workload too, unpin the blueprint or withdraw it explicitly.
+Cordon governs automatic placement; pin is an explicit operator decision. When the two collide (a blueprint pinned to a cordoned node), pin wins. The blueprint stays on (or, once confirmed, deploys onto) the pinned node even though the node is otherwise unschedulable. This keeps cordon's cost predictable: cordoning a node cannot silently break a workload you already chose to anchor there. If you want to remove the workload too, unpin the blueprint or withdraw it explicitly (subject to the same confirmation rule as any other removal).
### Pinning a stateful blueprint
-Pinning a stateful blueprint that is currently deployed on multiple nodes shrinks the desired set to one node. On the next reconciliation tick, the non-pinned deployments enter `evict_blocked` and wait for an explicit eviction confirmation. This is the same flow that protects stateful workloads from automatic withdraw when a selector changes. Confirm each eviction from the deployment table or unpin the blueprint to restore the original desired set.
+Pinning a stateful blueprint that is currently deployed on multiple nodes shrinks the desired set to one node. Once you confirm the resulting rollout, the non-pinned deployments enter `evict_blocked` and wait for an explicit eviction confirmation from the deployment table. This is the same flow that protects stateful workloads from automatic withdraw when a selector changes. Confirm each eviction from the deployment table or unpin the blueprint to restore the original desired set.
## Troubleshooting
+
+ This is expected. Pinning, unpinning, and any placement change that uncordoning surfaces only update what the reconciler *wants* to do; they never execute on their own. Open the blueprint in **Fleet → Deployments**, click **Apply now**, review the rollout preview, and click **Confirm Apply**. Until you do, the blueprint's catalog tile and detail sheet footer show **pending** or **reapproval required**.
+
+
+ A blocker is present. The most common cause after a pin change is an offline or unreachable target node: the preview elevates a create or remove action on an offline node to a blocker and disables Confirm Apply until it clears. Check the node's status in Fleet → Overview, or change the pin to a reachable node. An in-progress deploy on another node (`Deploy in flight`) surfaces as a warning, not a blocker, and does not by itself block confirmation.
+
- Check whether the node is cordoned. Cordoned nodes are excluded from new placements; the Federation tab summary lists every cordoned node and the reason. Uncordon the node to re-enable automatic placement, or pin the blueprint explicitly to deploy onto a cordoned node.
+ Check whether the node is cordoned. Cordoned nodes are excluded from new placements; the Federation tab summary lists every cordoned node and the reason. Uncordon the node to make it eligible again, then confirm the resulting rollout preview, or pin the blueprint explicitly to deploy onto a cordoned node (also subject to confirmation).
- Check whether the blueprint is pinned to that node. Pin overrides cordon by design, so a blueprint pinned to a cordoned node still deploys on the next reconciliation tick. Unpin the blueprint to restore selector-driven placement, then cordon will be honoured.
+ Check whether the blueprint is pinned to that node. Pin overrides cordon by design, so a blueprint pinned to a cordoned node still deploys once the pin's rollout is confirmed. Unpin the blueprint to restore selector-driven placement, then cordon will be honoured.
- The pin is the source of truth. Open Federation and confirm the **Pinned to** value matches your intent. The **Effective** column shows what the reconciler will use. Selector matches are ignored while a pin is set.
+ The pin is the source of truth. Open Federation and confirm the **Pinned to** value matches your intent. The **Effective** column shows what the reconciler will pursue once authorized. Selector matches are ignored while a pin is set.
That is the stateful guard working as intended. The reconciler does not auto-evict stateful workloads; each leftover deployment must be confirmed from the deployment table, just like a selector change would require. Unpinning the blueprint restores the original desired set if you want to keep all of them.
@@ -176,13 +201,16 @@ Pinning a stateful blueprint that is currently deployed on multiple nodes shrink
The Fleet Overview grid polls every 30 seconds, so the `Cordoned` pill can lag the action by up to that long. The Federation tab summary and the audit log update immediately; if the audit log shows the cordon action but the badge is still missing, click **Refresh** on the Fleet page to force an immediate re-fetch.
- Federation is available on every tier under **Fleet → Federation**. Cordon and pin mutations require an admin user (or node-admin for cordon on nodes they manage). If the tab is missing, refresh the Fleet page; if controls are read-only, sign in as an admin.
+ Federation lives under **Fleet → Federation** on every Sencho instance. Cordon and pin mutations require an admin user (or node-admin for cordon on nodes they manage). If the tab is missing, refresh the Fleet page; if controls are read-only, sign in as an admin.
Federation reads from the same Blueprints registry that powers **Fleet → Deployments**. If the Deployments tab shows blueprints but Federation does not, refresh the Fleet page (the Pin policy table caches its source list when the tab mounts). If Deployments is also empty, no blueprint has been saved yet; create one there first.
- The pin clears automatically when its target node is removed from the fleet. The blueprint reverts to its selector behaviour on the next reconciliation tick. There is no manual cleanup step.
+ The pin clears automatically when its target node is removed from the fleet. The blueprint reverts to its selector behaviour, subject to a fresh rollout confirmation before anything actually redeploys. There is no manual cleanup step.
+
+
+ Those action names don't exist. Sencho's audit log records the HTTP method, path, and a plain-language summary ("Cordoned node," "Uncordoned node," "Updated blueprint pin") rather than a fixed action taxonomy. Use the Audit view's search box with a keyword like "cordon" or "pin" instead of a formal filter value.
@@ -198,7 +226,7 @@ Federation is one tab in a larger Fleet view, and it focuses on a narrow slice o
| [Sencho Mesh](/features/sencho-mesh) | Cross-node container networking so a service on one node can reach a service on another by alias. | Mesh routes packets between containers; Federation decides which nodes receive blueprint deployments. |
| [Fleet Actions](/features/fleet-actions) | Bulk operations across labelled nodes (restart, stop). | Fleet Actions runs imperative operations on existing deployments; Federation steers declarative placement decisions. |
| [Fleet Sync](/features/fleet-sync) | Push-only replication of security policies (scan policies, CVE suppressions) from a control instance to replica instances. | Fleet Sync replicates *security state*, not placement; the two features do not interact. |
-| [Blueprints](/features/blueprint-model) | The declarative deployment model whose reconciler Federation steers. | Required reading: without Blueprints, Federation has nothing to do. |
+| [Blueprints](/features/blueprint-model) | The declarative deployment model whose reconciler Federation steers, including the rollout-preview and Confirm Apply mechanic every Federation change flows through. | Required reading: without Blueprints, Federation has nothing to do, and the confirm step described throughout this page is defined there. |
| [Licensing](/features/licensing) | Community and Admiral plans. | How fleet capabilities relate to plans. |
Federation is not Fleet Sync, not Mesh, and not the remote-node proxy. The cordon flag affects only declarative blueprint deployments, not manually deployed stacks. If you are looking for a way to take an entire node fully out of service (existing deployments included), see the deployment table's withdraw flow on each affected blueprint; cordon by itself is intentionally non-destructive.
diff --git a/docs/features/fleet-sync.mdx b/docs/features/fleet-sync.mdx
index 00bec894..a452e985 100644
--- a/docs/features/fleet-sync.mdx
+++ b/docs/features/fleet-sync.mdx
@@ -11,10 +11,10 @@ Fleet Sync replicates three resources today, all over the same channel and with
- **CVE suppressions** ([CVE Suppressions](/features/cve-suppressions)).
- **Misconfig acknowledgements**.
-Fleet Sync lives on the **Security** page (the **Policies** and **Suppressions** tabs) on both the control (where you author rules) and the replica (where you see them, read-only).
+Fleet Sync lives on the **Security** page (the **Policies** and **Suppressions** tabs, the latter also holding misconfig acknowledgements) on both the control (where you author rules) and the replica (where you see them, read-only).
-
+
## 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/` with username `system` and the source fingerprint in the summary) and the control's server logs. A built-in status panel is on the post-1.0 roadmap.
+- **The built-in status signal is a rollup, not a push-by-push history.** [Fleet View](/features/fleet-view)'s **Status** tab shows a **Policy sync** row per remote (`In sync`, `degraded`, or `paused`), but it only tells you the current state, not the history of individual pushes. For that detail, use the receiving replica's audit log (every applied push records `POST /api/fleet/sync/` with username `system` and the source fingerprint in the summary) or the control's server logs.
- **Stack-pattern scope only in the form UI.** The scan policy and CVE suppression forms expose a stack-pattern glob (e.g. `prod-*`) but no node selector. The underlying data model supports node-scoped rules, and they replicate faithfully when present, but authoring them today requires writing directly to the control's API.
- **5,000 rows per resource per push.** If the control accumulates more than 5,000 active rules in any one resource, the sender truncates to the first 5,000 and emits a `warning` notification (cool-down: 6 hours). In practice no fleet should approach this cap; if you see the warning, treat it as a signal to consolidate rules.
- **Pilot-agent nodes are not part of Fleet Sync today.** The control logs a one-time warning per pilot node and skips it during fanout. Replicating security rules over the [pilot tunnel](/features/pilot-agent) is on the post-1.0 roadmap.
@@ -150,7 +164,7 @@ Fleet Sync v1 ships the three replicated resources and the control mechanics des
The push for the delete probably failed. Check **Settings → Nodes** on the control; a remote that is offline or whose token is invalid will not have received the delete. Once the underlying issue is resolved, the next retry catches the remote up to the current state (which no longer includes the deleted rule).
- There is no first-party sync-status panel in the UI today (see [Limitations](#limitations)). Two surfaces give you the information indirectly: the replica's **Audit** view records every applied push as a `POST /api/fleet/sync/` entry with username `system` and the source fingerprint in the summary, and the control's server logs record the push outcome per remote. The administrative API endpoint `GET /api/fleet/sync-status` returns the per-resource per-node status table for tooling that needs to consume it directly.
+ Open the [Fleet View](/features/fleet-view) **Status** tab: each remote's card shows a **Policy sync** row reading `In sync`, `degraded`, or `paused`, with a tooltip naming the last error when degraded. For push-by-push detail beyond that rollup (see [Limitations](#limitations)), two surfaces give you the information directly: the replica's **Audit** view records every applied push as a `POST /api/fleet/sync/` entry with username `system` and the source fingerprint in the summary, and the control's server logs record the push outcome per remote. The administrative API endpoint `GET /api/fleet/sync-status` returns the per-resource per-node status table for tooling that needs to consume it directly.
The control side of Fleet Sync caps each push at 5,000 rows per resource (see [Limitations](#limitations)). If your suppression list, scan policy list, or acknowledgement list exceeds the cap, the sender truncates to the first 5,000 and emits a single warning every 6 hours. Consolidate rules by replacing repetitive entries with pattern-scoped ones, or remove suppressions that are no longer needed.
diff --git a/docs/features/fleet-view.mdx b/docs/features/fleet-view.mdx
index 834db864..d93dabb3 100644
--- a/docs/features/fleet-view.mdx
+++ b/docs/features/fleet-view.mdx
@@ -3,14 +3,14 @@ title: Fleet View
description: Monitor every node in your Sencho deployment from one screen, drill into stacks and containers, and orchestrate Sencho version updates across the fleet.
---
-The **Fleet** tab is the single page you open when you want to see the whole estate at once: every node's health, every stack, every container, and which boxes need a Sencho update. It is the home for fleet-wide tabs that go beyond monitoring (Snapshots, Status, Deployments, Routing, Federation, Actions, Secrets), each linking out to its own dedicated page.
+The **Fleet** tab is the single page you open when you want to see the whole estate at once: every node's health, every stack, every container, and which boxes need a Sencho update. It is the home for fleet-wide tabs that go beyond monitoring (Snapshots, Docker Labels, Deployments, Routing, Federation, Actions, Secrets), each linking out to its own dedicated page.
Fleet is hub-only and is hidden from the nav strip when a remote node is the active selection. See [Multi-Node Management](/features/multi-node#what-top-level-views-show-when-a-remote-node-is-active).
-
+
## 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 ` nodes · online · last sync Xs`, where the sync delta uses compact units (`s` / `m` / `h`). While data is loading the line reads `syncing…` instead.
- A **reasons line** below the meta line that names exactly what is wrong, e.g. `1 offline · 1 critical`. The reasons line is hidden when the fleet is Healthy.
- Three **stat tiles** on the right edge, in mono tabular numerals:
@@ -38,14 +37,15 @@ A single rail summarises the state of every registered node so you can read the
### Tabs
-The Fleet view is a tab strip. Every tier sees Overview, Status, Map, Deployments, Federation, and Actions. Snapshots appears for admins. Routing and Secrets are limited-availability fleet surfaces and are not part of the default tab strip. A vertical separator after **Status** divides the per-node monitoring tabs from the fleet-wide orchestration tabs.
+The Fleet view is a tab strip. Every tier sees Overview, Status, Map, Docker Labels, Deployments, Federation, and Actions. Snapshots appears for admins. Routing and Secrets are limited-availability fleet surfaces and are not part of the default tab strip. A vertical separator after **Docker Labels** (or after **Map** when Docker Labels is not present) divides the per-node monitoring tabs from the fleet-wide orchestration tabs.
| Tab | Tier | What it does |
|-----|------|--------------|
| **Overview** | Community | The grid or topology view of every node and its health. Covered in the next section. |
-| **Snapshots** | Community | Snapshot every compose file across the fleet. See [Fleet-Wide Backups](/features/fleet-backups). |
+| **Snapshots** | Community (admin role) | Snapshot every compose file across the fleet. See [Fleet-Wide Backups](/features/fleet-backups). |
| **Status** | Community | One card per node summarising which automations and security features are configured. Covered below. |
| **Map** | Community | A read-only map of how stacks, services, networks, volumes, and ports relate across the fleet, with anomaly flags. Covered below. |
+| **Docker Labels** | Community | Estate-wide audit of Docker and Compose labels across every node. See [Docker Label Audit](/features/docker-label-audit). |
| **Deployments** | Community | Blueprint deployments and reconciler state. See [Blueprints](/features/blueprint-model). |
| **Routing** | Limited availability | Cross-node service routing via Sencho Mesh when that surface is enabled on the instance. See [Sencho Mesh](/features/sencho-mesh). |
| **Federation** | Community | Cordon nodes and pin blueprints to specific hosts. See [Fleet Federation](/features/fleet-federation). |
@@ -54,13 +54,14 @@ The Fleet view is a tab strip. Every tier sees Overview, Status, Map, Deployment
### Action buttons
-Three buttons sit next to the tab strip in the top-right corner. They are visible from every tab, not just Overview.
+Two buttons sit next to the tab strip in the top-right corner. They are visible from every tab, not just Overview.
| Button | What it does |
|--------|--------------|
-| **Check Updates** | Opens the [Node Updates sheet](#node-updates) where you can manage Sencho version updates across the fleet. |
| **Refresh** | Forces an immediate re-fetch of fleet data. The icon spins while the refresh is in flight; the button is disabled until it completes. |
-| **Add node** | Opens the same Add Node dialog as **Settings · Infrastructure · Nodes** so you can register a new node without leaving the Fleet page. Admin-only. After saving a remote proxy node, a connection test runs automatically and the result toasts in (success or saved-but-unreachable). |
+| **Export Dossier** | Admin-only. Walks every node and stack and downloads a `homelab-dossier.zip` Markdown archive of the whole fleet. See [Fleet Dossier](/features/fleet-dossier). |
+
+The **Node Update** and **Manage nodes** buttons live in the Overview tab's own toolbar rather than this shared row; see [Toolbar](#toolbar) below.
## The Overview tab
@@ -68,15 +69,17 @@ Overview is the default tab and is where most operators spend their time. It off
### Toolbar
-In **Grid** mode the toolbar exposes search, sort, filters, and the view toggle. In **Topology** mode only the view toggle remains (the grid-only controls collapse).
+In **Grid** mode the toolbar exposes search, sort, filters, and the view toggle. In **Topology** mode only the view toggle remains (the grid-only controls collapse). **Node Update** and **Manage nodes** sit at the end of the row in both modes.
| Control | Behaviour |
|---------|-----------|
-| **Search** | Real-time filter that matches against node names and the names of stacks deployed on each node. Typing `plex` keeps only nodes that have a `plex` stack; typing `dev` keeps only nodes whose name contains `dev`. |
+| **Search** | Collapsed to a single icon button by default; click it to expand the input (it stays expanded while a query is entered and collapses again on blur once cleared). Filters in real time against node names and the names of stacks deployed on each node. Typing `plex` keeps only nodes that have a `plex` stack; typing `dev` keeps only nodes whose name contains `dev`. |
| **Sort** | Combobox with five orderings: Name, CPU Usage, Memory Usage, Containers, Status. The selection persists in the browser. |
| **Direction toggle** | Arrow button next to the sort combobox. The icon rotates 180° and the title flips between *Switch to ascending* / *Switch to descending* to confirm the current direction. |
-| **Filters** | Popover with four sections: **Status** (All / Online / Offline), **Type** (All / Local / Remote), **Severity** (Critical Only toggle), and **Tags** (multi-select of fleet label palette, only visible if labels exist). The button shows a count badge for active filters; a **Clear all filters** action appears at the bottom of the popover when at least one filter is set. |
-| **Grid / Topology** | Segmented control on the right, with a Grid icon and a Topology icon. The control resets to Grid when the page is reloaded. |
+| **Filters** | Popover with five sections: **Status** (All / Online / Offline), **Type** (All / Local / Remote), **Severity** (Critical Only toggle), **Networking** (All / Exposed / Unknown / Drift, matching the [networking badge](#grid-view) on each card), and **Tags** (multi-select of fleet label palette, only visible if labels exist). The button shows a count badge for active filters; a **Clear all filters** action appears at the bottom of the popover when at least one filter is set. All filter and sort selections persist in the browser. |
+| **Grid / Topology** | Segmented control, with a Grid icon and a Topology icon. The control resets to Grid when the page is reloaded. |
+| **Node Update** | Opens the [Node Updates sheet](#node-updates) and immediately re-checks every node's version, same as **Recheck** inside the sheet. |
+| **Manage nodes** | Admin-only. Navigates to **Settings → Infrastructure → Nodes**, where you register, edit, or remove nodes. |
### Grid view
@@ -89,12 +92,15 @@ Every node renders as a card. The local node is pinned at the top of the grid wi
| **Type badge** | Outline pill reading `local` or `remote`. |
| **Version badge** | The node's Sencho version in mono tabular numerals (e.g. `v0.76.3`). Hidden if the node cannot report a version. |
| **Update available** badge | Warning pill shown when a newer Sencho release is published for this node. |
+| **Pinned** badge | Shown instead of **Update available** when the node's own Sencho image is pinned to a digest (or an unrecognised pin) and cannot be updated automatically from here. A tooltip explains why. |
| **Critical** badge | Destructive pill with a triangle icon, surfaced when the online node is above 90% CPU or 90% disk. |
| **Cordoned** badge | Warning pill with a Ban icon, surfaced when the node is cordoned. The badge tooltip carries the cordon reason or the default *Unschedulable: new blueprint deployments skip this node*. See [Fleet Federation](/features/fleet-federation) for the full cordon and pin flow. |
-| **Updating / Updated / Failed** badge | Update progress indicator, shown only while or just after an update flows through. Failed states surface inline retry and dismiss buttons and a cursor-following error tooltip. |
+| **Networking** badge | Warning pill reading `Networking · exposed`, `Networking · drift`, or `Networking · unknown exposure`, shown when the node has a stack with published ports, detected network drift, or unresolved exposure. Click it to jump to that node's Networking page. |
+| **Updating / Updated / Failed / Timed out** badge | Update progress indicator, shown only while or just after an update flows through. Failed and timed-out states surface inline retry and dismiss buttons and a cursor-following error tooltip. |
+| **Skipped** badge | Shown on the card when an available update has been deferred from the [Node Updates sheet](#node-updates); the same skip state is reflected there. |
| **Container stats grid** | Three cells: **Running** (active containers), **Stopped** (exited containers), **Stacks** (count, or `-` if the node has not reported). Hidden on offline nodes. |
| **CPU / RAM / Disk bars** | Each row shows the metric icon, the percent (CPU) or `used / total` (RAM, Disk), and a horizontal bar that tints amber at 60%, destructive at 80% (CPU/RAM), or amber at 75% / destructive at 90% (Disk). Hidden on offline nodes. |
-| **Update to v…** button | Outline button that runs along the bottom of the card when an update is available. The label includes the latest version. |
+| **Update to v…** button | Admin-only outline button that runs along the bottom of the card when an update is available. The label includes the latest version. |
| **Stack details** trigger | Footer button that toggles the stack drill-down. The label carries the stack count for the node. |
Offline nodes render dimmed, with no stats grid, no usage bars, and no update affordance.
@@ -108,25 +114,27 @@ Every card carries a three-dot **Node actions** kebab in the top-right corner. T
| **Edit node** | Opens the Edit dialog prefilled with the node's connection details. For proxy-mode remotes, saving with a changed API URL or token re-runs the connection test automatically. |
| **Delete node** | Opens a destructive confirmation. The local (default) node has no Delete option. Deleting a remote only removes it from this console; the remote instance and its containers are untouched. |
| **Cordon node** / **Uncordon node** | Marks the node unschedulable so new blueprint deployments skip it. Existing deployments keep running. Requires the `node:manage` permission (admin, or node-admin when scoped to that node). |
+| **Mute** submenu | Mute node notifications, mute update notifications, mute monitor alerts for this node, or open the full mute-rule manager. Shown to whoever can manage mute rules for the node. See [Alerts & Notifications](/features/alerts-notifications). |
-Edit and delete remain admin-only. Users without `node:manage` and without edit/delete affordances see no kebab on the card.
+Edit and delete remain admin-only. Users without `node:manage`, without mute permission, and without edit/delete affordances see no kebab on the card.
### Topology view
Switch the segmented control to **Topology** to swap the grid for a hub-and-spoke graph. The local node sits in the centre with a cyan ring and the remotes radiate outward; the layout is computed automatically from the registered fleet.
-
+
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 ` stacks` counter once the list has loaded.
-
+
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 ` policy/policies` | Always (when not gated) |
| **Backup** | `Enabled` or `Disabled` | Local node only |
| **Crash detect** | `On` or `Off` | Always |
+| **Policy sync** | `In sync`, or a `degraded` / `paused` pill with a tooltip naming the cause. See [Fleet Sync](/features/fleet-sync). | Remote nodes that have received at least one sync push |
Offline nodes show a muted card with the heading and the message `Node is unreachable. Configuration unavailable.` The Online or Offline indicator in the card header confirms reachability at the time of the last fetch.
@@ -239,10 +248,10 @@ The map is fleet-wide: the control instance gathers each node's view and merges
## Node Updates
-Click **Check Updates** in the page header to open the **Node Updates** sheet. From here you can read every node's current Sencho version, see which nodes have an update available, and trigger updates one node at a time or across the whole fleet. The sheet has two tabs: **Nodes** (the node table and summary) and **Changelog** (release notes for the available Sencho version, fetched from the GitHub Releases page).
+Click **Node Update** in the Overview tab's toolbar to open the **Node Updates** sheet. From here you can read every node's current Sencho version, see which nodes have an update available, and trigger updates one node at a time or across the whole fleet. The sheet has two tabs: **Nodes** (the node table and summary) and **Changelog** (release notes for the available Sencho version, fetched from the GitHub Releases page).
-
+
### 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.
- Triggering updates (per-row and **Update all**), skipping versions, and unskipping versions all require the **admin** role. Viewer and operator roles can read update status, the changelog, and the skipping state but cannot change them.
+ **Recheck**, triggering updates (per-row and **Update all**), skipping versions, and unskipping versions all require the **admin** role. Viewer and operator roles can read update status, the changelog, and the skipping state but cannot change them or force a recheck.
## How fleet data is fetched
diff --git a/docs/features/global-observability.mdx b/docs/features/global-observability.mdx
index bc9a3654..52c8a01f 100644
--- a/docs/features/global-observability.mdx
+++ b/docs/features/global-observability.mdx
@@ -11,7 +11,7 @@ The **Logs** tab aggregates output from the containers Sencho manages into a sin
-
+
## 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 `/` path in a tooltip; the displayed name is the compose service name with the project prefix and replica suffix stripped.
+- A **container name** in brand cyan. Hover to see the full `/` path in a tooltip; the displayed name is the compose service name with the project prefix stripped, and the `-1` / `_1` replica suffix removed for a service's first replica. A service scaled beyond one replica keeps its numeric suffix (`worker-2`, `worker-3`) so you can tell the copies apart.
- A **message** containing the raw log output. `STDERR`-source lines render in destructive rose; `STDOUT`-source lines render in stat-value white.
A row's whole-row tint follows its detected level. Error rows pick up a subtle destructive wash, warning rows pick up an amber wash, info rows have no tint and gain a subtle accent on hover. The detected level is independent of the source, so an `STDOUT` line that contains the word `ERROR:` still gets the rose row tint.
@@ -96,7 +96,7 @@ The viewport autoscrolls to follow new rows. If you scroll up to read older entr
A floating control strip is anchored to the bottom-right of the feed.
-
+
| 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 ` banner appears so you know more matches exist behind the slice.
-- **Followed containers** capped per stream. When the hub manages more containers than the cap, the feed follows the first set and posts a `Following of managed containers` notice so you know the feed is truncated; use the per-container log viewer for the rest.
+- **Followed containers** capped at sixty per stream. When the hub manages more than sixty containers, the feed follows the first sixty and posts a `Following of managed containers; not shown` line into the feed itself (as a synthetic warning row from a `sencho` entry, not a passive banner) so you know the feed is truncated; use the per-container log viewer for the rest.
When you need to chase older lines than the buffer holds, jump to the [per-container log viewer](/features/editor#log-viewer) in the Editor tab or run `docker compose logs` from the [Host Console](/features/host-console).
@@ -160,8 +160,8 @@ The page is built from several independent loops so the masthead and the sparkli
The polling fallback got an HTTP error when it tried to read `/api/logs/global`. Check that the host's Docker daemon is reachable on the socket Sencho is configured to use. The page retries the request every five seconds; once it succeeds, the banner clears on its own.
-
- `Idle` only checks whether an event has arrived in the last ten seconds. If your fleet is genuinely quiet (a maintenance window, a single low-traffic container), the dot rests gray and the band headers in the feed roll up to `2M AGO`, `5M AGO`, and so on. The next event flips the dot back to brand cyan and the state word back to `Streaming` on the next one-second tick.
+
+ `Idle` only checks whether an event has arrived in the last ten seconds. If your fleet is genuinely quiet (a maintenance window, a single low-traffic container), the rail rests gray and the band headers in the feed roll up to `2M AGO`, `5M AGO`, and so on. The next event flips the rail back to a shimmering brand cyan and the state word back to `Streaming` on the next one-second tick.
Severity is detected once per line by scanning the message body. Some loggers print `ERROR` as part of a stack trace header on a line that does not itself match the parser's keyword set, in which case the line is classified by its source: `STDOUT` falls back to `INFO`. To force the row to your preferred level, change the application's log format to something the parser recognises (`level=error`, `[ERROR]`, or a leading `Error:` / `Exception:`). The behavior is intentional: it keeps a noisy `STDOUT` line that quotes the word `ERROR` from polluting the error count.
@@ -172,8 +172,14 @@ The page is built from several independent loops so the masthead and the sparkli
**Clear** records the click time and hides every row whose timestamp is earlier than that moment, but the SSE stream keeps delivering events whose timestamps were generated by Docker before the click. A line emitted at `16:51:43.987` that arrives at the browser at `16:51:44.020` will still pass the filter even if you clicked **Clear** at `16:51:44.000`. The lag is normally a few hundred milliseconds; click **Clear** again to hide them.
+
+ Some applications color their console output with ANSI escape sequences. Sencho strips the non-printable escape character so it cannot corrupt the terminal-style rendering, but the printable characters that followed it (the color code and the closing reset, e.g. `[36m` or `[0m`) are left in the message text. This is cosmetic only; it does not affect level detection or search. If the noise bothers you, disable colored logging in the source application (most loggers respect `NO_COLOR` or a `--no-color` flag) or configure it to log JSON instead.
+
+
+ The replica-suffix strip only removes a trailing `-1` or `_1`, the suffix Docker Compose assigns to a service's first replica. Additional replicas (`-2`, `-3`, and so on) keep their numeric suffix so you can tell the copies apart in the feed and in the Stacks filter.
+
- The feed shows the containers Sencho manages (those started from its compose directory). A container started outside Sencho, or one belonging to a stack on a different node, does not appear. Open it from the [per-container log viewer](/features/editor#log-viewer) in the Editor, or use the [Host Console](/features/host-console) for ad-hoc `docker compose logs`.
+ The feed shows the containers Sencho manages (those started from its compose directory). A container started outside Sencho, or one belonging to a stack on a different node, does not appear. If the hub manages more than sixty containers, only the first sixty are followed; check for the `Following of managed containers` row. Either way, open the missing container from the [per-container log viewer](/features/editor#log-viewer) in the Editor, or use the [Host Console](/features/host-console) for ad-hoc `docker compose logs`.
When one container's follow stream drops (the container was removed, or the Docker daemon hiccuped), the feed posts a single warning row for that container and stops following it; the other containers keep streaming. Reopen the Logs tab to re-attach to every managed container.
diff --git a/docs/features/global-search.mdx b/docs/features/global-search.mdx
index cff5aeec..8d0d4f25 100644
--- a/docs/features/global-search.mdx
+++ b/docs/features/global-search.mdx
@@ -6,18 +6,19 @@ description: Jump to any page, node, or stack from anywhere in the app with a si
The **global search palette** lets you move around Sencho without reaching for the mouse. It covers the reachable page destinations for your tier and role (the same page list Classic top navigation, Smart primary and More, and mobile navigation use), every configured node, and every stack on every online node in your fleet. Compact launcher can also open **Settings** from its menu; Settings is not a palette page row.
-
+
## 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
@@ -81,10 +96,10 @@ Offline nodes show as greyed-out and are not selectable.
The fleet-wide stack list is gathered once when you begin a search and reused while you keep typing, so a stack created on a remote node after you started typing will not appear yet. Clear the query and search again to pull a fresh list from the fleet.
-
- The dot reflects the stack's run state: green for running, grey for exited, faded grey for unknown. Unknown means the node listed the stack but its status could not be read at that moment, usually a transient hiccup talking to that host's Docker engine. The stack is still selectable; open it to see its real state in the editor.
+
+ The dot reflects the stack's run state: green for running, grey for exited, faded grey for unknown, amber for partial. Partial means some of the stack's containers are up and some are not. Unknown means the node listed the stack but its status could not be read at that moment, usually a transient hiccup talking to that host's Docker engine. The stack is still selectable; open it to see its real state in the editor.
- The shortcut is handled at the window level, so it works from any screen, but a focused element that captures the keystroke first (for example a terminal in the host console) can intercept it. Click outside that element, or use the search icon in the top bar to open the palette instead.
+ The shortcut is handled at the window level, so it works from any screen, but it deliberately does nothing while focus is inside a text field: a text input, a text area, or the compose editor. This avoids hijacking a keystroke you're using to type (on macOS, Ctrl+K is also the native "delete to end of line" shortcut inside a field). Click outside the field first, or use the search icon in the top bar to open the palette instead.
diff --git a/docs/features/health-gated-updates.mdx b/docs/features/health-gated-updates.mdx
index fe051904..8abafdae 100644
--- a/docs/features/health-gated-updates.mdx
+++ b/docs/features/health-gated-updates.mdx
@@ -27,6 +27,7 @@ The verdict is computed from signals Sencho already tracks, so the check is fast
- **Current containers**: a container that is already unhealthy, restarting, or crashed before the update makes the result hard to evaluate; the dialog asks you to look first.
- **Healthcheck coverage**: how many services define healthchecks, which determines how thoroughly the post-update health gate can verify the result.
- **Pending update**: the [update preview](/features/auto-update-policies) classifies the pending change. A major version bump asks for review; patch updates and same-tag refreshes pass quietly.
+- **Local build services**: any service built from a local Dockerfile instead of pulled from a registry. The update rebuilds these from source, which depends on the local Dockerfile context and can take longer than a plain pull; this signal never blocks the verdict.
- **Rollback backup**: whether a backup slot exists and how old it is. A missing backup is only a note, because the update itself creates a fresh one when it starts.
- **Node disk**: disk usage near or above the node's alert threshold warns you before a large pull fills the disk.
@@ -39,7 +40,7 @@ On stacks with more than one Compose service, you can also open readiness for a
When the selected service is build-backed and has no registry update, the proceed button reads **Rebuild** instead of **Update**. A per-service update badge appears only when a registry update is confirmed for that service.
-
+
## 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.
Fresh installs land on the **Community** tier until you activate a license key. All Community features (unlimited nodes, compose editor, global logs, alerts, Custom OIDC, and more) are available immediately.
@@ -82,7 +73,7 @@ When your trial ends, the card you provided is automatically charged and your pl
Open **Settings → Admiral Account** in the Sencho dashboard. When the license is not active, the **Activate** section sits below the Plan card:
-
+
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: ` for active subscriptions, `TRIAL: Xd left` for trials, and `STATUS: expired` for expired licenses. |
+| **RENEWS** | Shown for an active, non-lifetime subscription with a renewal date. |
+| **DURATION** | Shown as `lifetime` in place of RENEWS for a legacy license with no expiry. |
+| **TRIAL** | `Xd left` while a trial is active. |
+| **STATUS** | `expired` for an expired license. |
+
+Community and disabled licenses show only the SCOPE and PLAN pills.
The **Plan** card lists:
-- **Tier name** (e.g. `Sencho Admiral`) with a short status line, such as "Active license on this control plane", "Trial: X days remaining", or "Your license has expired."
-- **Customer**: the customer name on the purchase.
-- **Product**: the purchased product (e.g. `Sencho Admiral`).
-- **License key**: the last four characters of your key, displayed as `****-****-****-XXXX`. The full key is never re-displayed after activation.
+- **Tier name** (`Sencho Community` or `Sencho Admiral`) with a status line, such as "Community plan. Full AGPLv3 self-hosted control plane.", "Active license on this control plane.", "Trial: X days remaining.", "Your Admiral license has expired...", or "Your license has been disabled...".
+- **License** (Community only): a link to view the AGPLv3 source on GitHub.
+- **Recovery Vault** (Admiral only): confirms the entitlement is included in your subscription.
+- **Hardened Build** (Admiral only): a **Switch to Hardened** button; see [Switching to Hardened Build](#switching-to-hardened-build) below.
+- **Current image** and **Channel**: the image reference and channel (`Community` or `Hardened`) this control plane is currently running.
+- **Customer**, **Product**, and **License key** (active licenses only): purchase metadata and your key masked to its last four characters (`****-****-****-XXXX`). The full key is never re-displayed after activation.
+
+## Switching to Hardened Build
+
+Hardened Build is a separate, entitlement-gated image channel available to Admiral instances: a Studio Saelix-published image with supply-chain assurance artifacts, distinct from the default Community image. Access is checked live against Studio Saelix's assurance service every time you use it, not just against your Admiral tier, so an Admiral license alone does not guarantee Hardened Build is enabled for your account.
+
+To switch:
+
+1. Open **Settings → Admiral Account** from an interactive admin session. API tokens cannot manage image channels.
+2. Click **Switch to Hardened** in the Plan card.
+3. Sencho checks your entitlement and registry access, then opens a review dialog listing the **current** image, the **target** Hardened image, your **registry** access status, the **compose path** that will be edited, and the **pin kind** (tag or digest). If the entitlement check fails, you'll see "Hardened image access is unavailable"; contact Studio Saelix.
+4. Confirm the switch. Sencho pulls the Hardened image using credentials from [Registries](/features/private-registries), rewrites your compose file, and recreates the service. This restarts Sencho.
+
+Back up first (a fleet snapshot or a copy of your compose file) since this rewrites your compose file and restarts the control plane. If the switch fails partway through, the Plan card shows an **Image operation** field with the failure reason and an **Acknowledge failure** button, which clears the failed state so you can retry.
+
+
+ Once a node is on the Hardened channel, only an interactive admin session on that node can pull further Hardened updates. Fleet's remote **Update all** and per-node **Update** actions cannot update a Hardened node from elsewhere in the fleet; update it directly on that node's own dashboard instead.
+
## Managing your subscription
-When the license is an active subscription, the Plan section action row exposes **Manage subscription** alongside **Deactivate**. **Manage subscription** opens the billing portal in a new tab, where you can update your payment method, view invoices, cancel, or switch plans.
+For an active, non-lifetime subscription, the Plan section's action row exposes **Manage subscription** alongside **Deactivate**. **Manage subscription** opens the Lemon Squeezy billing portal in a new tab, where you can update your payment method, view invoices, cancel, or switch plans.
The same portal is reachable from the profile menu in the top-right corner of the app:
-
+
-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.
- The agent has not dialled home yet. Three things can cause this. First, the enrollment token expired (15 minutes from generation): open **Edit node**, click **Regenerate enrollment token**, copy the new command, and run it on the remote host. Second, the `sencho-agent` container exited or never started; `docker ps -a` on the remote will show the exit code. Third, outbound HTTPS to the control instance is blocked, so check the remote's egress firewall or proxy.
+ The agent has not dialled home yet. Three things can cause this. First, the enrollment token expired (15 minutes from generation): open **Edit node**, click **Regenerate enrollment token**, save the new `compose.yaml`, and run `docker compose up -d` again on the remote host. Second, the `sencho-agent` container exited or never started; `docker ps -a` on the remote will show the exit code. Third, outbound HTTPS to the control instance is blocked, so check the remote's egress firewall or proxy.
The bearer token saved for the row no longer matches what the remote will accept. This usually means somebody clicked **Generate Token** on the remote (which invalidates the previous token) or the row was saved with a typo. Generate a fresh token on the remote, click the pencil icon on the row in the control instance, paste the new token into the API Token field, and save.
@@ -325,6 +330,6 @@ Sencho takes the opposite approach: infrastructure-level encryption (VPN, revers
The control instance's license tier is authoritative for proxied requests, so a Community control plane gates Admiral features on every remote, even if the remote itself has its own Admiral license. Activate an Admiral license on the control instance to lift the gate fleet-wide. The reverse case (Admiral control plane, Community remote) works automatically because the control plane's tier is what the remote trusts.
- That is intentional. Account, Admiral Account, Users, SSO, API Tokens, Registries, Recovery Vault, Nodes, Routing, and Webhooks are control-plane concerns and are hidden while a remote node is selected. Switch back to **Local** from the node switcher to manage them. The full list of which panels are per-node, per-browser, and control-plane-only lives in the [What Settings apply per node](#what-settings-apply-per-node) table above.
+ That is intentional. Account, Admiral Account, Users, SSO, API Tokens, Nodes, Registries, Recovery Vault, Routing, Mute Rules, Webhooks, and Recovery are control-plane concerns and are hidden while a remote node is selected. Switch back to **Local** from the node switcher to manage them. The full list of which panels are per-node, per-browser, and control-plane-only lives in the [What Settings apply per node](#what-settings-apply-per-node) table above.
diff --git a/docs/features/node-compatibility.mdx b/docs/features/node-compatibility.mdx
index f4e5a11f..c7d345c4 100644
--- a/docs/features/node-compatibility.mdx
+++ b/docs/features/node-compatibility.mdx
@@ -7,7 +7,7 @@ When you manage [multiple nodes](/features/multi-node), each running its own Sen
## How it works
-Every Sencho instance exposes a public `GET /api/meta` endpoint that advertises its version and a list of **capabilities**, one flag per feature it implements. When you switch to a node, your control instance fetches that node's metadata and caches it (briefly on the server, and for 5 minutes on success or 30 seconds on failure in the dashboard), so the version pill and the per-feature gates stay in sync without re-hitting the remote on every navigation. Testing a node from **Settings · Nodes**, or a node finishing an update, refreshes its metadata right away rather than waiting for the cache to expire.
+Every Sencho instance exposes a public `GET /api/meta` endpoint that advertises its version and a list of **capabilities**, one flag per feature it implements. When you switch to a node, your control instance fetches that node's metadata and caches it (3 minutes on the server, and 5 minutes on success or 30 seconds on failure in the dashboard), so the version pill and the per-feature gates stay in sync without re-hitting the remote on every navigation. Testing a node from **Settings · Nodes**, or a node finishing an update, refreshes its metadata right away rather than waiting for the cache to expire.
Features whose capability is missing from the active node's list are replaced by a lock card naming the missing feature and prompting you to upgrade the node. Core features (stack management, containers, resources, logs) work on every Sencho version and are never gated this way.
@@ -22,17 +22,17 @@ Features whose capability is missing from the active node's list are replaced by
The node switcher popover shows each node's Sencho version next to its type chip, separated by a middle dot.
-
+
-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 |
- `vulnerability-scanning` is advertised only when the Trivy binary is present on the node. If a node does not have Trivy installed, it omits this capability and the scanning UI is replaced by a lock card.
+ `vulnerability-scanning` is advertised only when the Trivy binary is present on the node. If a node has no scanner installed, it omits this capability and the scanning UI is replaced by a lock card whose body reads "Install a scanner from the Scanner setup tab." instead of the generic upgrade message.
-Nodes that do not advertise `guided-external-network-preflight` keep legacy deploy behavior (no guided missing-external dialog). When the capability is present but the check cannot run, deploy fails closed instead of guessing.
+
+ A node connected in [Pilot Agent](/features/multi-node#add-a-remote-node-pilot-agent) mode never advertises `host-console`, even after upgrading, because that feature's control-to-agent path is not yet wired through the enrollment tunnel. This is the one capability gap that upgrading the node cannot close; it only closes on a Distributed API Proxy connection.
+
+
+A handful of capabilities gate a smaller piece of behavior rather than a whole panel, so a missing one falls back to an older behavior instead of a lock card:
+
+- Nodes that do not advertise `guided-external-network-preflight` keep legacy deploy behavior (no guided missing-external dialog). When the capability is present but the check cannot run, deploy fails closed instead of guessing.
+- Nodes that do not advertise `service-scoped-update` fall back to the legacy per-container layout on multi-service stacks: no declared-service headers, and no per-service update, rebuild, or restore.
+- Nodes that do not advertise `update-guard` run manual updates directly, with no pre-update rollback-readiness dialog.
+- `cross-node-rbac` is a security boundary, not a convenience: the control instance refuses to forward a non-admin request, or a confirmed stop-by-label, to a remote node that does not advertise it. This stops a mixed-version fleet from letting a lower-privileged action escalate on an un-upgraded node.
## Handling nodes that do not advertise metadata
diff --git a/docs/features/overview.mdx b/docs/features/overview.mdx
index 3c4ff465..a7b43368 100644
--- a/docs/features/overview.mdx
+++ b/docs/features/overview.mdx
@@ -1,5 +1,6 @@
---
title: Product Guide
+sidebarTitle: Overview
description: A high-level tour of Sencho, organized by the same areas as the product guide sidebar.
---
diff --git a/docs/features/pilot-agent.mdx b/docs/features/pilot-agent.mdx
index ee6b33ff..083b2cde 100644
--- a/docs/features/pilot-agent.mdx
+++ b/docs/features/pilot-agent.mdx
@@ -105,7 +105,7 @@ When you submit the Add Node form with Mode set to Pilot Agent, the control inst
3. Shows the compose file in an enrollment dialog with a 15-minute countdown.
-
+
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
- Tear the agent down on the wrong host (`docker compose -f compose.yaml down -v` from the directory you saved the compose file in), then regenerate the enrollment token on the control instance and deploy the new compose file on the correct host.
+ Tear the agent down on the wrong host (`docker compose down -v` from the directory you saved the compose file in), then regenerate the enrollment token on the control instance and deploy the new compose file on the correct host.
@@ -320,7 +320,7 @@ The generic node-connectivity issues (a node showing Offline, a pilot agent stuc
- Deleting the node removes the routing entry on the control instance but does not touch the remote. The agent still holds `/app/data/pilot.jwt` and will keep dialing. Tear the agent down on the remote (`docker compose -f compose.yaml down -v` from the directory holding the compose file) so the `sencho-agent-data` named volume that stores the persisted token is removed.
+ Deleting the node removes the routing entry on the control instance but does not touch the remote. The agent still holds `/app/data/pilot.jwt` and will keep dialing. Tear the agent down on the remote (`docker compose down -v` from the directory holding the compose file) so the `sencho-agent-data` named volume that stores the persisted token is removed.
diff --git a/docs/features/remote-updates.mdx b/docs/features/remote-updates.mdx
index 1fda8a8e..c1de64d4 100644
--- a/docs/features/remote-updates.mdx
+++ b/docs/features/remote-updates.mdx
@@ -11,6 +11,10 @@ This page covers the mechanism: prerequisites, what happens on a node during an
Triggering an update (per-row or **Update all**) requires the **admin** role. Viewer and operator roles can read update status but cannot dispatch updates.
+
+ This is the Sencho self-update feature, reached from the Fleet tab. The top-level **Update** nav item is a different feature: it tracks pending Docker image updates for your own stacks (Plex, Radarr, and the rest), not the Sencho binary itself. See [Health-Gated Updates](/features/health-gated-updates) for that page.
+
+
## Prerequisites
A node can self-update when all of the following are true:
@@ -23,13 +27,13 @@ Nodes deployed with `docker run`, with hand-rolled systemd units, or with orches
## Anatomy at a glance
-Open the **Fleet** tab and click **Check Updates** in the page header to open the **Node updates** sheet. The sheet lists every registered node with its current and latest Sencho version, the cluster-wide summary, and the action buttons.
+Open the **Fleet** tab and click **Node Update** in the Overview toolbar to open the **Node updates** sheet. The sheet lists every registered node with its current and latest Sencho version, the cluster-wide summary, and the action buttons, split across two tabs: **Nodes** (the table and summary) and **Changelog** (release notes for the latest published version, pulled from GitHub Releases).
-
+
-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.
-
-
-
+
+ If the local node is running the Admiral **Hardened Build** image instead of a Community image, clicking **Update** here runs a different, entitlement-gated switch flow instead of the compose-repin steps below: it requires an authenticated browser session (not an API token) and can fail with its own codes (`entitlement_denied`, `preflight_mismatch`, `compose_unavailable`, `registry_access_unavailable`) surfaced on the same **Failed** badge. See [Licensing · Feature breakdown](/features/licensing#feature-breakdown) for what Hardened Build is.
+
-After **Update & restart** is confirmed:
+After **Update & restart** is confirmed on a Community-channel install:
1. The browser captures the gateway's current boot timestamp from `/api/health`.
2. The server classifies the compose image pin. For semver pins it pulls the target release, stages a rewritten compose file, and spawns a short-lived helper container that copies the file onto the host and runs `docker compose up -d --force-recreate`.
@@ -65,7 +69,7 @@ After **Update & restart** is confirmed:
If the pull or compose rewrite fails before the container restarts, the overlay dismisses within a few seconds and surfaces the error in a toast instead of waiting for the full reconnect timeout.
-If the new container does not come up within 5 minutes, the overlay surfaces an **Update timed out** message with a *Try Reloading* button so the page is never left waiting indefinitely. If the gateway can detect that the update did not even start (for example, the image pull failed before the helper container could spawn) it surfaces a **Failed** badge with the underlying error on the Local card. The badge appears as soon as the helper writes its error file, or by the 3-minute mark at the latest, instead of waiting for the full 5-minute timeout.
+If the new container does not come up within 5 minutes, the overlay surfaces a **Taking longer than expected** message with a **Reload to check** button so the page is never left waiting indefinitely; this is not necessarily a failure, since a large image pull can legitimately run past that window. If the gateway can detect that the update did not even start (for example, the image pull failed before the helper container could spawn) it surfaces a **Failed** badge with the underlying error on the Local card. The badge appears as soon as the helper writes its error file, or by the 3-minute mark at the latest, instead of waiting for the full 5-minute timeout.
The self-update helper container inherits all bind mounts from the main Sencho container 1:1. If your `docker-compose.yml` references `env_file`, `configs`, or `secrets` outside the compose working directory, those host paths must be mounted into the Sencho container at the *same container path* as on the host. See [Troubleshooting](/operations/troubleshooting#local-self-update-fails-with-env-file-not-found) if you encounter `env file not found` errors during a local update.
@@ -77,6 +81,9 @@ If the new container does not come up within 5 minutes, the overlay surfaces an
Fleet self-update respects how each node compose file declares the Sencho image. Semver pins are rewritten to the target release before recreate. Floating tags such as latest are pulled without changing the compose file. Digest pins and unresolved interpolated values block automatic updates; those rows show a **Pinned** badge instead of an **Update** button.
The image pull always runs before any compose rewrite, so a failed pull never leaves the compose file half-updated.
+
+A node running the Admiral **Hardened Build** image is a deliberate exception: it is digest-pinned like any other locked reference, but it does not show the **Pinned** badge and its **Update** button stays available, because that path routes through the Hardened Build's own entitlement-gated switch instead of a compose repin. See the note in [Updating the local (gateway) node](#updating-the-local-gateway-node) above.
+
## What happens during an update
For both local and remote nodes, an update goes through the same three steps:
@@ -123,7 +130,7 @@ The header of the sheet exposes a **Recheck** button that does three things in o
The self-update helper container is launched with the same bind-mount set as the main Sencho container, but only paths that are explicitly mounted are visible inside the helper. If your `docker-compose.yml` references an `env_file` (or `configs`, or `secrets`) at a host path that is *not* mounted into the Sencho container, the helper cannot read it and the recreate step fails. Mount the referenced host directory into the Sencho container at the same container path (the 1:1 rule), redeploy Sencho once, and retry the update. The full walk-through with example mounts is in [Troubleshooting](/operations/troubleshooting#local-self-update-fails-with-env-file-not-found).
- The overlay polls `/api/health` every 3 seconds for up to 5 minutes. If it has not cleared by then, the new container is either still pulling a very large image, refusing to start, or crashing on boot. Open a host shell on the gateway and run `docker logs ` to inspect the boot output, then `docker ps` to confirm a new container ID is running. The overlay's *Try Reloading* button bypasses the timer once the container is healthy again.
+ The overlay polls `/api/health` every 3 seconds for up to 5 minutes. If it has not cleared by then, the new container is either still pulling a very large image, refusing to start, or crashing on boot. Open a host shell on the gateway and run `docker logs ` to inspect the boot output, then `docker ps` to confirm a new container ID is running. The overlay's **Reload to check** button bypasses the timer once the container is healthy again.
The remote answered the dispatch but did not restart in time. The two most common causes are a registry pull error (rate limit, private-registry auth missing, network egress blocked) or a Compose file that fails validation under the new version. Open a shell on the remote and run `docker compose pull` followed by `docker compose up -d` against the Sencho working directory; the error printed there is the same one Sencho captured. Click **Retry** on the row once the cause is fixed.
diff --git a/docs/features/resources.mdx b/docs/features/resources.mdx
index 0ef140c8..8596496e 100644
--- a/docs/features/resources.mdx
+++ b/docs/features/resources.mdx
@@ -6,12 +6,12 @@ description: Browse, filter, scan, and clean up Docker images, volumes, and unma
The **Resources** tab shows everything Docker is storing on your host, broken down by ownership, with one-click cleanup and on-demand vulnerability scanning.
-
+
## 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.
Sencho protects its own image, network, and named volumes from accidental deletion. The matching rows carry a **Sencho** pill alongside the managed status and the delete control is disabled.
@@ -68,14 +70,14 @@ A **Scan history** button sits on the right of the tab strip when image vulnerab
### Images
-Lists all Docker images on the host with their ID, repository tag, size, and status.
+Lists all Docker images on the host with their ID, repository tag, size, and status. Click **Repository:Tag**, **Size**, or **Status** to sort.
-**Filter chips:** `All`, `Managed`, `External`. Each chip shows the matching count, for example `All 31 · Managed 14 · External 17`.
+**Filter chips:** `All`, `Managed`, `External`. Each chip shows the matching count, for example `All 18 · Managed 15 · External 3`.
**Status column** combines two pieces of information per row:
- A usage badge: `In Use` plus one chip per Sencho stack that uses the image (click a chip to open that stack), `In Use` plus `External` when it runs only in another Docker project, or `Unused` when no container references it.
-- A severity badge from any image scan: `Clean`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`, or `UNKNOWN`. Click the badge to open the scan results.
+- A severity badge from any image scan: `Clean` (no findings at all), a CVE severity (`LOW`, `MEDIUM`, `HIGH`, `CRITICAL`, or `UNKNOWN`), or `Findings` (no CVEs, but a secret or misconfiguration finding). Click the badge to open the scan results.
**Per-row actions** sit on the right:
@@ -87,36 +89,37 @@ Lists all Docker images on the host with their ID, repository tag, size, and sta
Click the eye icon on any image row to open a detail sheet with these sections:
+
+
+
+
- **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.
+
Deleting a volume is permanent. Any data stored in it will be lost. Always back up important volume data before pruning.
#### Browse volume contents
-Click the folder icon on any volume row (admin-only) to open a read-only browser for that volume. The panel shows a directory tree on the left and a file viewer on the right.
+Click the folder icon on any volume row (admin-only) to open a read-only browser for that volume. The panel shows a directory tree on the left and a file viewer on the right. For an anonymous volume, a band above the tree shows the full 64-character name with a copy button, since the sheet's title and breadcrumb only show the shortened form.
- **Read only.** No edits, no writes, no deletions. The browser launches a short-lived helper container with the volume mounted read-only.
- **Size cap.** Files are read up to 5 MB. Larger files show a truncation banner.
@@ -131,7 +134,7 @@ Click the folder icon on any volume row (admin-only) to open a read-only browser
### Unmanaged
-
+
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 › `` › Runs**. The sheet header reports the total run count, and a **Download CSV** secondary action exports the full history. The footer reads **Next run ``** when one is scheduled.
-
+
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
Start / Bring Up Stack runs `docker compose up -d` against the stack's compose directory. If the stack folder has been removed or its compose file is missing, the run fails. Re-create or restore the stack from the [Editor](/features/editor), then click **Run now** on the schedule to confirm it succeeds before the next cron tick.
+
+ Auto-update Stack and Start / Bring Up Stack run through the same [deploy-enforcement](/features/deploy-enforcement) scan-policy gate as a manual deploy. If the stack's current images match an active blocking policy, the run fails with a message naming the policy and the offending images, and a matching warning notification is dispatched. Review or adjust the policy, or update the flagged images, then click **Run now** to retry.
+
+
+ Sencho serializes operations per stack. If a manual deploy, restart, or other lifecycle action is already in progress for a stack when its scheduled task fires, the schedule skips that turn and records a failure naming the operation that was already running, instead of queuing behind it. No action is needed: the task fires again at its next cron tick. Click **Run now** if you want to retry immediately once the other operation finishes.
+
Backup Stack Compose Files keeps a single slot per stack under `/backups//`. Each run overwrites the previous backup. This is by design: for a versioned archive of compose files across the fleet, use a [Create Fleet Snapshot](/features/fleet-backups) task, which retains every snapshot under the snapshots directory.
diff --git a/docs/features/security.mdx b/docs/features/security.mdx
index 2491bfda..f1a69e59 100644
--- a/docs/features/security.mdx
+++ b/docs/features/security.mdx
@@ -8,7 +8,12 @@ capabilities that power [vulnerability scanning](/features/vulnerability-scannin
center, so you can answer "what should I look at first?" without hunting through other pages. It is
scoped to the active node: the findings and scanner status you see reflect whichever node is selected.
-The page is organized into tabs.
+
+
+
+
+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).
@@ -104,12 +111,12 @@ Right-click any stack (or open the kebab that appears on hover) for its context
-
+
## 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.
- Each node has a 10-stack pin limit. When you pin an 11th stack, the oldest pin is automatically evicted so the new one fits. Unpin a stack you no longer need before adding another, or accept that the oldest will roll off.
+ Each node has a 10-stack pin limit. When you pin an 11th stack, the oldest pin is automatically evicted so the new one fits, and a toast confirms which one rolled off. Unpin a stack you no longer need before adding another, or accept that the oldest will roll off.
- Shortcuts only fire when a stack row is selected in the sidebar (one row is highlighted). They are blocked while a text input is focused or the global command palette (Ctrl+K) is open. Click a stack row, then try the shortcut again.
+ Most shortcuts only fire when a stack row is selected in the sidebar (one row is highlighted); click a row and try again. B, Ctrl+A, and Esc are the exception and work without a selection. Every shortcut is blocked while a text input is focused or the global command palette (Ctrl+K) is open.
+
+
+ Stack rows show only the status pill and name; per-row label dots were removed. To see or change a stack's labels, look at which group it's sorted into, or right-click the row (or use its kebab) and open the **Labels** submenu. See [Stack Labels](/features/stack-labels) for the full picture.
+
+
+ Mute (and the label group header's mute kebab) only render for admins on nodes that support notification suppression. Sign in with an admin account, or update the node if it's running a version that predates notification suppression.
Click the **+** icon to the right of the search box to bring them back. The chip row collapses to a **−** / **+** toggle, and the state is remembered in your browser, so an earlier collapse persists across reloads until you expand it again.
@@ -179,4 +203,7 @@ Click the ticker in any other state to navigate directly to the linked view.
Sencho tried to reach those nodes during the search but they did not respond. The node may be offline, its connection credentials may have expired, or a network change may have interrupted the link. Open the Fleet view to check each node's status, or go to Settings and test the connection from the Nodes panel.
+
+ Nodes Sencho already shows as offline are skipped before the fan-out starts, so they're excluded from both the results and the unreachable warning rather than counted as a failed attempt. Reconnect the node first, then search again.
+
diff --git a/docs/features/stack-activity.mdx b/docs/features/stack-activity.mdx
index 62e09da1..3a0e1a17 100644
--- a/docs/features/stack-activity.mdx
+++ b/docs/features/stack-activity.mdx
@@ -3,10 +3,10 @@ title: Stack Activity
description: Per-stack event log showing deploys, restarts, config changes, and alerts, each attributed to the user or system process that triggered them.
---
-The **Activity** tab in the right-hand **Anatomy** panel gives you a timestamped record of everything that happened to the selected stack: deploys, restarts, stops, starts, image updates, drift events, and health gate outcomes. Every entry is attributed to the user or background process that triggered it, so you always have full operational context at a glance. The **files** and **edit** buttons on the same header strip belong to the panel as a whole and stay available regardless of which tab is active.
+The **Activity** tab in the right-hand **Anatomy** panel gives you a timestamped record of everything that happened to the selected stack: deploys, restarts, stops, starts, take-downs, image updates, drift events, and health gate outcomes. Every entry is attributed to the user or background process that triggered it, so you always have full operational context at a glance. The **files** and **Edit compose** buttons on the same header strip belong to the panel as a whole and stay available regardless of which tab is active.
-
+
## 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 ` for user-initiated actions; `via ` for events triggered by background processes such as Auto-Heal or the Scheduler |
| **Time** | Relative time since the event: `just now`, `5m ago`, `2h ago`, `3d ago` |
+If a [notification mute rule](/features/alerts-notifications#mute-rules) blocks delivery of a matching alert, the entry still appears here with a **Suppressed** badge next to the message. Hovering the badge shows which rule matched. Suppression only affects delivery to the bell and external channels; the activity row itself is never suppressed.
+
## Event categories
-Ten categories have a dedicated icon. Any other notification scoped to a stack (deploy failures, scan findings, monitor alerts) also flows into the timeline and renders with a generic activity icon. Automatic creation of missing external networks during deploy writes a history-only activity row (`network_auto_created`) on the stack timeline; it does not appear in the notification bell.
+Eleven categories have a dedicated icon. Any other notification scoped to a stack (deploy failures, scan findings, monitor alerts) also flows into the timeline and renders with a generic activity icon. Automatic creation of missing external networks during deploy writes a history-only activity row (`network_auto_created`) on the stack timeline; it does not appear in the notification bell.
| Category | Example message |
|----------|-----------------|
| Deploy succeeded | `plex deployed` |
| Stack restarted | `dozzle restarted` |
| Stack stopped | `radarr stopped` |
+| Stack taken down | `radarr taken down` (or `radarr taken down (volumes removed)` when the take-down included volume removal) |
| Stack started | `radarr started` |
| Image update applied | `Auto-update: stack "dozzle" updated with new images` |
| Drift detected | `Drift detected on sonarr: 2 findings` |
@@ -72,7 +75,7 @@ Two states can make the tab look empty, and they mean different things:
Sencho attributes every recorded event to the actor that performed it:
-- **User-initiated actions** (deploy, restart, stop, start, image update triggered from a button or the API): the actor is the authenticated username, shown as `by `.
+- **User-initiated actions** (deploy, restart, stop, start, take down, image update triggered from a button or the API): the actor is the authenticated username, shown as `by `.
- **Background subsystems**: the actor is the subsystem name, shown as `via `.
| Background subsystem | Displayed as |
@@ -120,6 +123,6 @@ Cleanup runs on the same schedule as container-metrics cleanup, so retention app
These are two distinct events. The first is a manual restart; the second is Auto-Heal restarting the container after a separate unhealthy-duration breach. Each event is its own entry with its own actor. Two rows here are correct behavior, not a duplicate.
- The notification bell surfaces system alerts and error-level events. User-initiated operational events (start, stop, restart, deploy, update) appear exclusively in the per-stack Activity tab, keeping the global tray focused on conditions that need your attention.
+ The notification bell surfaces system alerts and error-level events. User-initiated operational events (start, stop, restart, take down, deploy, update) appear exclusively in the per-stack Activity tab, keeping the global tray focused on conditions that need your attention.
diff --git a/docs/features/stack-dossier.mdx b/docs/features/stack-dossier.mdx
index 415c9536..e0035b9d 100644
--- a/docs/features/stack-dossier.mdx
+++ b/docs/features/stack-dossier.mdx
@@ -24,11 +24,11 @@ The top of the tab shows a read-only summary derived live from the stack's Compo
| Fact | What is shown | Source |
|------|--------------|--------|
| **Services** | Names of declared services | Compose file service keys |
-| **Ports** | Count and all published host ports across all services | `ports:` entries, host-side bindings only |
+| **Ports** | A count of every `ports:` entry declared across services, followed by each port number | `ports:` entries. The count is not filtered to host-published mappings: a container-only port (declared without a host side) is counted and listed the same way as a published one. |
| **Volumes** | Count of mounted volumes | `volumes:` references across services |
-| **Network** | Stack's primary network name and type (for example, `arr-net · bridge`) | Compose network definitions |
+| **Network** | Stack's primary network name, shown with a fixed `bridge` label (for example, `arr-net · bridge`) | Compose network definitions. The `bridge` label is fixed text, not read from the network's actual driver. |
| **Restart** | Restart policy of the first service with one set | `restart:` key |
-| **Env file** | The env file path, its variable count, and names of any `${VAR}` with no supplied value | `.env` / `env_file:` at the stack root |
+| **Env file** | The env file path, its variable count, and a count of any `${VAR}` with no supplied value. The Markdown export additionally lists the missing variable names; the live tab shows the count only. | `.env` / `env_file:` at the stack root |
| **Source** | `local`, or the Git remote URL, branch, and path when the stack is linked | Stack's git source configuration |
**Multi-file Compose stacks:** when a stack has a Git source with multiple Compose files, Sencho merges them (equivalent to `docker compose config`) before reading facts. Services, ports, and volumes shown in the Generated Facts reflect the merged result, not any individual file.
@@ -80,7 +80,7 @@ Two actions in the tab header produce a single Markdown document combining the g
- **copy md** copies the document to the clipboard.
- **download** saves it as `-dossier.md`.
-The exported document always includes the Generated Facts and all Operator Notes. When the Networking tab capability is available on the active node, the export also includes a network exposure summary. When the Storage tab capability is available, the export also includes a storage summary. Both additions are automatic when the capability is present.
+The exported document always includes the Generated Facts and all Operator Notes. When the active node advertises the [Compose Networking](/features/compose-networking) capability, the export also includes a network exposure summary; when it advertises the [Storage Portability](/features/compose-storage) capability, the export also includes a storage summary. Both additions are automatic when the capability is present, but each summary is fetched fresh at export time rather than cached, so a capable, up-to-date node can still omit a section if that fetch fails. See Troubleshooting below.
The export is clean Markdown that reads well in Git, Obsidian, BookStack, a README, or stored alongside a backup. The generated facts never include `.env` values, only variable names and counts. Your operator notes are exported exactly as you write them, so treat them like any document you might share and avoid pasting secrets you do not want in the export.
@@ -106,6 +106,10 @@ Fleet Snapshots can optionally capture dossier notes alongside Compose files. Wh
Events from the Drift tab (drift detected, drift resolved) appear in the stack's **Activity** timeline alongside deploys and restarts, giving a chronological view of when the stack drifted and when it recovered.
+### Rollback readiness
+
+At the bottom of the Dossier tab, on nodes that advertise the `update-guard` capability, a **Rollback readiness** section reports an overall **Ready**, **Partial**, or **Not ready** state for the stack, built from six signals: whether a compose backup exists, whether that backup covers the stack's env file, whether the image tag is pinned or moving, whether the last deploy succeeded, whether any service defines a healthcheck, and an explicit note that application data in named volumes and bind mounts is never restored by a rollback. It shares the same capability-plus-fetch-failure behavior described above for the Networking and Storage export summaries. See [Health-Gated Updates](/features/health-gated-updates#rollback-readiness) for what each signal means and how to act on it.
+
## Permissions and per-node scoping
Any authenticated user with `stack:read` permission can view a stack's dossier. Read-only users see all fields and the export buttons, but no Save button appears.
@@ -120,7 +124,6 @@ Dossiers are stored per stack and per node. A stack named `plex` on your home no
- **Documentation drift checks Access URLs only.** The port-validity check applies only to the Access URLs field. It does not validate other fields or check hostnames.
- **No history on operator notes.** The dossier stores only the current version of each field. Sencho does not record change history for operator notes. Use a version-controlled wiki or Git if you need note history.
- **Export requires a parseable Compose file.** When the Compose file cannot be parsed, the export buttons are disabled. Operator notes are unaffected and still save normally.
-- **Rollback readiness requires a specific capability.** The rollback readiness section is shown only when the `update-guard` capability is active for the node.
## Accessing the Dossier tab
@@ -154,7 +157,7 @@ The Anatomy panel is visible in the right column when a stack is open and the ed
When a stack's compose file publishes a port through a variable such as `${PLEX_PORT}:32400`, Sencho does not know the resolved value from the Compose source, so it treats that port as unverifiable and stays quiet. If your access URL refers to the same port and a warning still appears, the URL's port does not match any statically declared published port. Either update the URL or pin the port in the Compose file.
- The Markdown export includes a network exposure summary and a storage summary only when the corresponding capabilities are available on the active node. If those sections are missing, the capabilities are not enabled for that node. The core generated facts and all operator notes are always included regardless of capabilities.
+ A missing section has one of two causes. Either the active node does not currently advertise the [Compose Networking](/features/compose-networking) or [Storage Portability](/features/compose-storage) capability, because it is running an older Sencho build or was briefly unreachable when Sencho last checked, or the node does have the capability but the on-demand fetch for that summary failed at export time (a transient API, proxy, or Docker issue). Retry the export before assuming the node needs an upgrade. The core generated facts and all operator notes are always included regardless of either cause.
Fleet snapshot restore has two steps: restoring Compose files and optionally restoring dossier notes. Notes are restored only when you explicitly choose to restore them at restore time. If you did not select that option, or if the original snapshot was captured without documentation enabled, the notes will not be present. To restore notes, choose a snapshot that includes documentation and confirm the notes restore option during the restore flow.
diff --git a/docs/features/stack-drift.mdx b/docs/features/stack-drift.mdx
index 77018165..ae7c5897 100644
--- a/docs/features/stack-drift.mdx
+++ b/docs/features/stack-drift.mdx
@@ -81,6 +81,10 @@ If the Compose file declares a tag (`:latest`, `:1.25`) and the running containe
A Compose port range such as `8000-8002:8000-8002` is compared conservatively. Because the runtime expands ranges into individual ports, the comparison can read as a difference even when the deployment is correct. Sencho errs toward surfacing the possible difference rather than hiding it.
+### Network findings and the Networking tab
+
+The **network-undeclared** and **network-missing** findings reuse the same comparison the stack's [Networking](/features/compose-networking) tab uses, so the two surfaces never disagree about a network attachment. The difference is history: this tab persists findings in the drift ledger over time, while the Networking tab always shows the current live state with no history. A stack with an open network finding also counts toward the **Drift** chip in the Fleet Overview's Networking filter group, so a network-attachment mismatch is visible both per-stack here and across the fleet there.
+
## When drift is recorded
**Opening the tab** runs the spatial engine and displays the current state. It does not write anything to the ledger or update the history.
@@ -117,20 +121,24 @@ Drift events also appear in the stack's **Activity** tab alongside deploys, rest
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
Blueprints have a separate policy-based drift mode (observe, suggest, enforce) distinct from the runtime drift detection described on this page.
+
+ The Networking tab's runtime drift section uses the same network comparison as the findings on this page, without the persisted history.
+
diff --git a/docs/features/stack-file-explorer.mdx b/docs/features/stack-file-explorer.mdx
index 02c1a490..a9fa8762 100644
--- a/docs/features/stack-file-explorer.mdx
+++ b/docs/features/stack-file-explorer.mdx
@@ -3,23 +3,23 @@ title: Files & Volumes
description: Browse and safely edit the config files inside a stack's mounted volumes, plus the files in the stack's own directory, from the dashboard.
---
-Files & Volumes gives you direct access to the files your containers actually read at runtime: the configuration inside a stack's **mounted volumes** (bind mounts and named Docker volumes), as well as the files in the stack's own source directory. A typical use is editing the Nginx and proxy configuration inside a reverse proxy's `/config` volume without leaving the dashboard. It lives on the **Files & Volumes** tab inside the stack editor, alongside the dedicated `compose.yaml` and `.env` editors.
+Files & Volumes gives you direct access to the files your containers actually read at runtime: the configuration inside a stack's **mounted volumes** (bind mounts and named Docker volumes), as well as the files in the stack's own source directory. A typical use is editing the Nginx and proxy configuration inside a reverse proxy's `/config` volume without leaving the dashboard. It lives on the **Files** tab inside the stack editor, alongside the dedicated `compose.yaml` and `.env` editors.
-
+
Browsing is limited to the roots the stack itself declares: its source directory and the volumes mounted by its services. You cannot browse other stacks or arbitrary locations on the host.
-## Opening Files & Volumes
+## Opening the Files tab
1. Click any stack in the left sidebar.
-2. Click **edit** in the right-hand Anatomy panel header to open the editor.
-3. Switch to the **Files & Volumes** tab.
+2. Click **Edit compose** in the right-hand Anatomy panel header to open the editor.
+3. Switch to the **Files** tab.
-The **files** shortcut button next to **edit** in the Anatomy header opens the editor and selects the Files & Volumes tab in one click.
+The **Files** shortcut button next to **Edit compose** opens the editor with the **Files** tab already selected. The shortcut only appears when your account has **stack read** permission on the stack; without it, the tab is not offered.
Files & Volumes is available on every Sencho tier. Read actions (browse, preview, download, inspect permissions) require **stack read** permission, which every signed-in role has by default. Write actions (upload, edit, create, rename, move, change permissions, delete) require **stack edit** permission on your account.
@@ -40,12 +40,13 @@ The same source mounted by more than one service is shown as a single root, summ
### When a volume cannot be browsed
-A volume root is shown with a short explanation and is not browsable when:
+A volume root shows a short explanation in place of its tree and is not browsable when:
- the bind mount points at a host path Sencho cannot reach (mount that path into the Sencho container to browse it),
-- the mount targets a protected host location such as `/`, `/etc`, `/proc`, `/sys`, `/dev`, or the Docker socket,
-- the mount overlaps Sencho's own managed stack area (browse the owning stack's source instead), or
-- a named volume cannot be resolved on the node that owns the stack.
+- the mount targets a single file rather than a directory (a bind that maps one file straight into the container has nothing to open a tree on),
+- the mount targets a protected host location: the filesystem root, kernel/OS state (`/etc`, `/proc`, `/sys`, `/dev`, `/run`, `/var/run`), the base-image binaries and libraries Sencho's own runtime depends on (`/usr`, `/bin`, `/sbin`, `/lib`, `/lib64`, `/boot`, `/root`), or the Docker socket,
+- the mount overlaps an area Sencho manages at runtime: its own application directory, its data directory, the OS temp directory, or a configured upload/scanner directory (browse the owning stack's source instead), or
+- a named volume cannot be resolved: either Sencho could not find it on the node that owns the stack, or it could not reach Docker to check (a transient daemon or proxy issue, distinct from the volume simply not existing).
### Named-volume editing details
@@ -53,10 +54,12 @@ Named-volume editing is best-effort and bound by file ownership. The helper cont
## Layout
-The Files & Volumes tab splits into two panes. The left pane holds the Browsing selector, the upload affordance, the **New file** and **New folder** buttons, and the directory tree for the selected root. The right pane is the action bar plus the file viewer.
+The Files tab splits into two panes. The left pane holds the Browsing selector, the upload affordance, the **New file** and **New folder** buttons, and the directory tree for the selected root. The right pane is the action bar plus the file viewer.
+
+A full-screen toggle sits next to **Close editor** whenever the Files tab is active. It collapses the identity, health, and log column on the left of the stack workspace so the tree and viewer use the full width, which helps when you are working with wide config files or a deep directory tree. Click it again (or switch to a different tab) to return to the normal two-column layout.
-
+
## 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.
Editing a file does not restart any containers. If your stack reads the file at runtime (for example, a config file mounted as a volume), restart the relevant service after saving so the container picks up the new content.
@@ -190,7 +195,9 @@ Select more than one entry to act on them together.
Bulk Move and Delete skip the protected stack files (`compose.yaml`, `.env`, and the rest) at the stack root, which are reported as kept rather than acted on; they can still be included in a Download.
-Each item is processed independently. If some succeed and others fail (for example, a permission error on one file), Sencho reports how many succeeded and leaves the items that failed selected so you can see which they were and retry. Very large selections are capped: a Download that would exceed the archive's file-count or total-size limit is refused before it starts.
+Each item is processed independently. If some succeed and others fail (for example, a permission error on one file), Sencho reports how many succeeded and leaves the items that failed selected so you can see which they were and retry.
+
+**Selection and archive limits.** A bulk action accepts at most 100 selected paths per request; select fewer items and repeat the action for a larger batch. A bulk Download is also capped at 5000 files and 1 GiB of uncompressed content: a selection that would exceed either limit is refused before the archive starts building, so narrow the selection or download large folders in smaller batches.
## Permissions (chmod)
@@ -248,14 +255,14 @@ The Permissions dialog opens for everyone; only users with stack edit permission
## Troubleshooting
-
- The signed-in role lacks **stack read** permission for this stack. Ask an admin to grant the permission on your account, or sign in with a role that already has it.
+
+ The signed-in role lacks **stack read** permission for this stack, so the tab is not offered; opening it directly (for example via a bookmarked link) redirects you back to the Anatomy tab. Ask an admin to grant the permission on your account, or sign in with a role that already has it.
The stack declares no volume Sencho can reach, so the explorer opens on the stack source. This happens when the stack uses only anonymous or `tmpfs` mounts, when its bind mounts point at host paths that are not mounted into the Sencho container, or when its named volumes have not been created yet. Deploy the stack so its named volumes exist, or mount the relevant host path into the Sencho container, then reopen the tab.
- The root carries a short explanation. A bind mount to a host path Sencho cannot reach needs that path mounted into the Sencho container. A mount that targets a protected host location (`/`, `/etc`, `/proc`, `/sys`, `/dev`, the Docker socket) or that overlaps Sencho's managed stack area is intentionally not browsable. A named volume that cannot be resolved on the owning node is not browsable until it exists.
+ The root carries a short explanation. A bind mount to a host path Sencho cannot reach needs that path mounted into the Sencho container. A bind that targets a single file rather than a directory has no tree to browse. A mount that targets a protected host location (the filesystem root, kernel/OS paths, Sencho's own runtime binaries, or the Docker socket) or that overlaps an area Sencho manages at runtime is intentionally not browsable. A named volume that cannot be resolved on the owning node is not browsable until it exists, or until a transient Docker connectivity issue clears.
Sencho writes to named volumes as an unprivileged helper user. A file owned by `root` or a specific application user may not be writable by that helper. Adjust the file's ownership or permissions on the host, or edit the file from a context that owns it, then try again.
@@ -269,8 +276,8 @@ The Permissions dialog opens for everyone; only users with stack edit permission
Another writer (a teammate, a deploy hook, or an out-of-band edit on the host) saved the file after you loaded it. Use the diff to compare your edits against the current version, then save again. Your typed buffer is kept so you do not have to retype your changes.
-
- The disk hosting the stack's compose directory has no free space. Check the volume mounted as `COMPOSE_DIR` (defaults to `/app/compose` inside the Sencho container) and free up space, or move the volume to a larger disk.
+
+ A full disk on the host backing the target directory surfaces as a generic write failure, not a dedicated error code. Check the volume mounted as `COMPOSE_DIR` (defaults to `/app/compose` inside the Sencho container) or the volume backing the target, free up space, or move it to a larger disk, then retry the upload.
The binary detection heuristic flagged null bytes or a high proportion of non-printable characters in the file's first kilobytes. This can happen with certain encodings, non-UTF-8 byte sequences, or unusual line-ending mixes. Click **Open as text anyway** on the binary panel to re-fetch the file as UTF-8 text and edit it in the inline viewer. Files above the 2 MB preview limit still need a Download.
@@ -297,6 +304,9 @@ The Permissions dialog opens for everyone; only users with stack edit permission
Bulk delete and move process each item independently and report how many succeeded. The items that failed stay selected so you can see which they were; open one to find out why (a common cause is a permission error on a named-volume file, or a name collision at the destination), fix it, and retry on the still-selected items.
- A bulk download is packed into a single archive with a fixed cap on both the number of files and the total uncompressed size. A selection that would exceed either cap is refused before the download starts. Narrow the selection, or download large folders in smaller batches.
+ A bulk download is packed into a single archive capped at 5000 files and 1 GiB of uncompressed content. A selection that would exceed either limit is refused before the download starts. Narrow the selection, or download large folders in smaller batches.
+
+
+ Bulk actions (download, move, delete) accept at most 100 selected paths per request. Select fewer items, or repeat the action across a few smaller batches.
diff --git a/docs/features/stack-labels.mdx b/docs/features/stack-labels.mdx
index 7e87cd5c..bf8944b5 100644
--- a/docs/features/stack-labels.mdx
+++ b/docs/features/stack-labels.mdx
@@ -3,11 +3,15 @@ title: "Stack Labels"
description: "Per-node tags that group your stacks by purpose, surface them under collapsible headers in the sidebar, and enable cross-stack bulk actions across the fleet."
---
-A **Stack Label** is a per-node tag (name plus color) applied to any stack. A labeled stack groups under that label's header in the sidebar instead of the flat list, and Fleet View can filter the overview by tag. Admins also get two label-driven fleet actions: stop every stack labeled `prod` across every node, or add a label to stacks across nodes in one shot.
+A **Stack Label** is a per-node tag (name plus color) applied to any stack. A labeled stack groups under that label's header in the sidebar instead of the flat list, and Fleet View can filter the overview by tag. Admins also get two label-driven fleet actions (stop every stack labeled `prod` across every node, or add a label to stacks across nodes in one shot) plus one-click shortcuts for muting a label's notifications.
+
+
+ Stack Labels are a different feature from **Compose Labels** (the raw `labels:` key-value metadata Docker Compose services declare) and from **Node Labels** (used for Blueprint placement). See [Docker Label Audit](/features/docker-label-audit) for the former; Stack Labels never read or write Compose-level labels.
+
## 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.
-A stack can carry multiple labels and will then appear under each label's group in the sidebar. There is no per-stack label cap; the only cap is the per-node total of 50.
+The **Labels** entry itself only appears in the context menu for users with edit rights on that stack (**admin** or **node-admin**); a viewer or deployer won't see it, even though they can still see label grouping and dots elsewhere. A stack can carry multiple labels and will then appear under each label's group in the sidebar. There is no per-stack label cap; the only cap is the per-node total of 50.
+
+## Muting a label's notifications
+
+Every Labels surface gives admins a shortcut into [Mute Rules](/features/alerts-notifications#mute-rules) scoped to a label, so a noisy group of stacks can be quieted without hand-building a rule. This requires the **admin** role and the active node advertising the `notification-suppression` capability; the mute controls simply don't render otherwise.
+
+Two patterns are used, both wired to the same underlying rule fields (label, and an **Apply to** target of Bell / External / Both):
+
+- **Instant presets**, available from the sidebar's per-label group-header kebab and from the stack context menu's **Mute label…** submenu (pick a label, then a preset): **Mute notifications for this label** (mutes bell and external delivery), **Mute external alerts for this label** (external channels only, bell stays active), and **Mute low-priority stack alerts** (mutes `info` and `warning` severities for the label, leaving `error` alerts audible). Each fires immediately and confirms with a toast; no dialog to review.
+- **Prefilled review**, available from **Manage label mute rules** (the same two menus) and from the bell-off icon on each row in **Settings · Organization · Labels**. Both open **Settings · Notifications · Mute Rules** with a **New mute rule** dialog already filled in for that label (name `Mute label: `, the label pre-selected, **Both** as the target) so you can adjust node scope, categories, or severity before saving.
+
+None of these delete or hide the underlying alerts; they only affect delivery to the bell, external channels, or both. See [Alerts & Notifications](/features/alerts-notifications#mute-rules) for the full Mute Rules field reference, suppression-vs-routing behavior, and how suppressed alerts still show up (marked **Suppressed**) in stack activity history.
## Fleet · Fleet Actions
@@ -99,27 +116,27 @@ Two cards in the **Fleet · Actions** tab use labels to drive cross-node operati
### 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.
+
+
+ Beyond the UI, an admin can drive one label at a time on the current node through the API: `POST /api/labels/:id/action` with `{ "action": "deploy" | "stop" | "restart", "dryRun": true }` runs that action across every stack carrying the label, node-locked against other bulk operations the same way the fleet cards are. There's no button for this yet, so it's reachable only via a **Full Admin** API token or a direct request from an admin session; see [API Tokens](/features/api-tokens) for how to mint one.
+
## Troubleshooting
@@ -139,11 +161,11 @@ When the chosen label name exists with different colors on different nodes, the
Grouping requires at least one stack to carry at least one label. With zero labels assigned the sidebar collapses into a single `UNLABELED` group, which is rendered as a flat list. Create a label from **Settings · Organization · Labels** or right-click any stack and use **Labels · New label**, assign it to a stack, and the grouped layout takes over.
-
- A stack row only renders trailing dots when at least one label is assigned to that stack. Right-click the row, open the **Labels** submenu, and tick at least one label; the dots appear on the next sidebar refresh. If a stack already has labels assigned but the dots still do not appear, check that the active node is the one that owns the assignments. Labels are per-node, so switching the node switcher to a different instance shows that instance's assignments only.
+
+ The **Labels** submenu (both viewing checkmarks and toggling assignment) requires **admin** or **node-admin** rights on that stack. Viewers and deployers can still see the sidebar grouping and the read-only Settings panel, but the context menu won't offer a way to change labels.
- The active node already has 50 labels (the per-node cap). Both the inline New label entry in the stack submenu and the + New label button in **Settings · Organization · Labels** hide themselves at the cap. Delete an unused label or rename an existing one to free a slot.
+ The active node already has 50 labels (the per-node cap). Both the inline New label entry in the stack submenu and the New label button in **Settings · Organization · Labels** hide themselves at the cap. Delete an unused label or rename an existing one to free a slot.
The Tags filter aggregates labels across every node in the fleet by name. If the new label only exists on one node and that node was offline at the moment the page loaded, the dropdown may not include it. Refresh **Fleet · Overview** with the toolbar refresh button to repull node state.
@@ -163,7 +185,27 @@ When the chosen label name exists with different colors on different nodes, the
The label-based Fleet Actions require the admin role. Confirm the active user has the admin role under **Settings · Users**; all other roles see the cards but cannot execute them.
+
+ Label muting requires the **admin** role and the active node advertising the `notification-suppression` capability. Update the node if it's on an older version, or confirm your role under **Settings · Users**.
+
Working as designed. The destructive confirmation reads `Removes the label from every stack across the fleet.` The label row and every assignment row that pointed to it are dropped in a single transaction. There is no undo; recreate the label by name and color and reassign the affected stacks if you need to recover.
+
+## Related features
+
+
+
+ See fleet label dots per stack in the drill-down, and filter the whole fleet by tag.
+
+
+ The full reference for every cross-node action, including Stop by label and Bulk label assign.
+
+
+ The complete Mute Rules field reference and how suppression interacts with routing.
+
+
+ Audit Compose-level and Node labels, the two label systems Stack Labels is deliberately kept separate from.
+
+
diff --git a/docs/features/stack-management.mdx b/docs/features/stack-management.mdx
index b2082379..0774066f 100644
--- a/docs/features/stack-management.mdx
+++ b/docs/features/stack-management.mdx
@@ -115,10 +115,10 @@ The image tag is taken from the final positional argument (for example `nginx:al
**Troubleshooting conversion:**
-
+
-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.
+
+
+ There is a second, separate limit in front of sign-in: Sencho also caps password and 2FA sign-in attempts from a single network address to 5 per 15 minutes. Because it is per-address rather than per-account, it is shared by everyone signing in from behind the same address (a shared office network, VPN egress, or NAT gateway). Once that shared budget is used up, sign-in attempts are rejected immediately with `Too many attempts. Please try again in 15 minutes.` for the rest of the window, for every account behind that address, regardless of the per-account MFA lockout described above.
+
## Account panel anatomy
@@ -216,7 +222,9 @@ The reset bumps the target user's token version, so any active sessions of their
## Lockout recovery
-The 15-minute lockout that fires after five failed verifications is per-account, not per-IP. The counter clears on a successful sign-in. Practical consequence: if you keep retrying with wrong codes after the 15-minute window expires, the counter is still at five and the lockout re-engages on the first wrong code. To break the cycle, either wait until you have a known-good code (clock synced authenticator) and sign in cleanly, or have an administrator reset your 2FA from the Users panel to clear both the enrollment and the failure counter at once.
+The 15-minute lockout that fires after five failed verifications is per-account, keyed to the user, not to where the request came from. The counter clears on a successful sign-in. Practical consequence: if you keep retrying with wrong codes after the 15-minute window expires, the counter is still at five and the lockout re-engages on the first wrong code. To break the cycle, either wait until you have a known-good code (clock synced authenticator) and sign in cleanly, or have an administrator reset your 2FA from the Users panel to clear both the enrollment and the failure counter at once.
+
+This is distinct from the shared, per-network-address limit described in [Rate limiting](#rate-limiting) above. An administrator reset clears the per-account lockout, but it cannot lift the shared per-address limit; that one only clears when its own 15-minute window elapses.
## Troubleshooting
@@ -231,6 +239,9 @@ The 15-minute lockout that fires after five failed verifications is per-account,
If Sencho runs in a container, the container's clock follows the host. Make sure the host clock is synced with NTP.
+
+ Sencho rejects a TOTP code that was already accepted once inside the same 30-second window, to prevent a captured code from being replayed. This is most often triggered by a double-submit on a slow connection or by re-typing a code you already used to sign in seconds earlier. Wait for your authenticator to generate the next code and try again. This rejection counts toward the five-attempt lockout counter, so repeated replays can still trigger the 15-minute lockout described in [Rate limiting](#rate-limiting).
+
Authenticator apps let you store many accounts. The label Sencho uses is the literal issuer `Sencho` plus your username. If you have more than one Sencho instance enrolled in the same authenticator, every entry shows the same issuer; rename them in the app to distinguish (most apps let you edit the label without invalidating the secret).
@@ -252,6 +263,9 @@ The 15-minute lockout that fires after five failed verifications is per-account,
- **Your own toggle.** **Settings · Account · Two-factor authentication · Require 2FA on SSO sign-in** is on for your account. Flip it off if SSO alone is enough for your threat model.
- **The provider's toggle.** Each SSO provider config has a `Require MFA` switch. When that is on, every SSO-provisioned user must enrol TOTP after their first successful sign-in. Only an administrator can change this on the provider config under **Settings · SSO**.
+
+ This is the shared per-network-address limit from [Rate limiting](#rate-limiting), not the per-account MFA lockout; it can appear on the plain username/password form before 2FA is even reached, and it affects every account signing in from that address. It clears on its own after 15 minutes. An administrator 2FA reset does not lift it, since it is not tied to any one account.
+
The 15-minute window expires on the clock, but the failure counter only resets on a successful sign-in. If you retry with another wrong code after the window expires, the counter is still at five and the lockout re-engages immediately. Have an administrator reset your 2FA from the Users panel to clear both the enrolment and the failure counter, then sign in with your password and re-enrol TOTP from your account settings.
diff --git a/docs/features/vulnerability-scanning.mdx b/docs/features/vulnerability-scanning.mdx
index 8cb43eab..a8781408 100644
--- a/docs/features/vulnerability-scanning.mdx
+++ b/docs/features/vulnerability-scanning.mdx
@@ -1,13 +1,13 @@
---
title: "Vulnerability Scanning"
sidebarTitle: Vulnerability scans
-description: "Scan container images and stack compose files for CVEs, secrets, and misconfigurations. Surface severity badges in the Resources Hub, compare scans over time, and gate deploys on policy violations."
+description: "Scan container images and stack compose files for CVEs, secrets, and misconfigurations. Surface severity badges in the Resources Hub, prioritize with known-exploited and EPSS evidence, compare scans over time, and gate deploys on policy violations."
---
-Sencho integrates with [Trivy](https://trivy.dev) to scan container images and Compose files for vulnerabilities (CVEs), hardcoded secrets, and misconfigurations. Findings surface as severity badges in the Resources Hub and as drillable reports in the scan drawer. The dedicated [Security page](/features/security) is the command center for risk review: overview, image findings, Compose risks, secrets, scan history, suppressions, and scanner setup all live there. Manual scanning, secret and misconfig detection, scan history, comparison, scheduled fleet scans, CVE suppressions, policy enforcement, SARIF export, single-scan SBOM export, and managed Trivy auto-update are all available on every tier.
+Sencho integrates with [Trivy](https://trivy.dev) to scan container images and Compose files for vulnerabilities (CVEs), hardcoded secrets, and misconfigurations. Findings surface as severity badges in the Resources Hub and as drillable reports in the scan drawer, enriched with known-exploited (KEV) and EPSS exploit-probability evidence so severity alone is not the only signal. The dedicated [Security page](/features/security) is the command center for risk review: an action-posture overview, image findings, Compose risks, secrets, scan history, suppressions, and scanner setup all live there. Manual scanning, secret and misconfig detection, scan history, comparison, scheduled fleet scans, CVE suppressions with OpenVEX triage, exploit intelligence, risk-based deploy policies, SARIF export, single-scan SBOM export, and managed Trivy auto-update are all available on every tier.
-
+
## 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 › `. The header surfaces the primary scan actions, the summary header reports severity counts and fixable findings, and the body groups findings across three tabs: **Vulnerabilities**, **Secrets**, and **Misconfigs**.
-
+
### 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.
+
+ A finding whose exploitability can't be confirmed fails **closed** on the known-exploited and fixable conditions: if the scan detail can't be read in full (a truncated cache hit or a database error), the gate treats KEV and fixable conditions as still active rather than silently skipping them. The severity condition still gates from the scan's stored aggregate either way.
+
+
### Creating a policy via the API
Policy CRUD endpoints are documented in the [Security API reference](/api-reference/security). A typical create call from CI looks like this:
@@ -221,20 +248,29 @@ curl -X POST https://your-sencho-instance:1852/api/security/policies \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
- "name": "prod-high-gate",
+ "name": "prod-risk-gate",
"stack_pattern": "prod-*",
"max_severity": "HIGH",
+ "block_on_severity": 0,
+ "block_on_kev": 1,
+ "block_on_fixable": 1,
"block_on_deploy": 1,
"enabled": 1
}'
```
+`max_severity` is only enforced when `block_on_severity` is `1`; it still must be one of `CRITICAL`, `HIGH`, `MEDIUM`, or `LOW`.
+
## Secret detection
Full scans ask Trivy to walk the image filesystem for patterns that look like hardcoded credentials, API tokens, cloud access keys, or private keys. Detection rules cover the common providers (AWS, GCP, GitHub, Slack, Stripe) plus generic high-entropy strings.
Trigger a full scan from the shield-icon menu on any image row with **Full scan (vulnerabilities + secrets)**. Findings appear on the **Secrets** tab of the drawer:
+
+
+
+
| 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.
- The block dialog names the policy that fired and lists every image that violated the threshold. Open the **Security** page → **Policies** tab and review the matching policy: check the stack pattern glob and the max severity. If the policy should not apply, tighten the pattern (for example `staging-*` instead of `*`) or turn **Block on deploy** off to keep the evaluation in alert-only mode. Admins can bypass a single deploy with the **Deploy anyway** button; every bypass is recorded in the [Audit Log](/features/audit-log) with the actor, policy, and violation list.
+ The block dialog names the policy that fired and lists every image that matched, along with which conditions matched (severity threshold, known-exploited, and/or fixable Critical/High). Open the **Security** page → **Policies** tab and review the matching policy: check the stack pattern glob and which block conditions are enabled. Remember that new policies default to blocking on known-exploited and fixable findings even when the severity threshold is off, so a CVSS-low finding can still trigger a block if it is on the KEV list. If the policy should not apply, tighten the pattern (for example `staging-*` instead of `*`), disable the condition that is too aggressive, or turn **Block on deploy** off to keep the evaluation in alert-only mode. Admins can bypass a single deploy with the **Deploy anyway** button; every bypass is recorded in the [Audit Log](/features/audit-log) with the actor, policy, and violation list.
+
+
+ These evidence tags depend on **exploit intelligence**, a daily background fetch of the CISA KEV catalog and FIRST EPSS scores. Confirm the toggle is on under the **Security** page → **Scanner setup** tab. If it is on but tags are still missing, the host may be offline or firewalled: the feature degrades gracefully rather than failing the scan, so existing tags keep showing but new CVEs will not get fresh KEV/EPSS data until connectivity is restored. Air-gapped hosts can leave the toggle off intentionally; severity, CVSS, and fix-availability data are unaffected either way.
Sencho fails open when Trivy is not installed on the target node, so operators are never locked out by tooling state. A warning alert is dispatched through your configured notification channels with the message `Pre-deploy scan for "" skipped: Trivy not installed on this node`. Install Trivy from the **Security** page → **Scanner setup** tab to enforce the policy; see [Installing Trivy](/operations/trivy-setup) for options.
diff --git a/docs/features/webhooks.mdx b/docs/features/webhooks.mdx
index 76fb768b..57f8af36 100644
--- a/docs/features/webhooks.mdx
+++ b/docs/features/webhooks.mdx
@@ -34,7 +34,7 @@ Open **Settings → Automation → Webhooks** and click **Create webhook**. The
After you click **Create**, the new webhook's secret appears once in a green callout titled **Webhook created. Copy your secret now.** Copy it before dismissing the callout; once you navigate away, it cannot be retrieved. The configured-webhooks list always shows the secret in masked form (`********` plus the last four characters).
-
+
### 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.
+
+ The webhook's action shares the stack's per-stack lock with manual actions from the UI. The webhook fired while a manual deploy, update, restart, stop, start, rollback, or backup on the same stack was still running, so Sencho skipped the webhook's action rather than queuing it behind the running one. Retry the trigger once the in-flight operation finishes. Git source sync does not hit this path; it waits for the in-progress operation instead of skipping.
+
+
The trigger endpoint is rate limited to 500 requests per minute per source IP. CI/CD runners behind shared NAT or a datacenter egress pool all share that budget. Either space the calls out, send them from a less crowded egress, or split the work across multiple webhooks if a single workflow fires hundreds of triggers in close succession.
diff --git a/docs/getting-started/configuration.mdx b/docs/getting-started/configuration.mdx
index db91a26e..6aaeb3d5 100644
--- a/docs/getting-started/configuration.mdx
+++ b/docs/getting-started/configuration.mdx
@@ -14,7 +14,7 @@ Operational settings (host alerts, data retention, image-update automation, mesh
| `COMPOSE_DIR` | Absolute path to the directory that contains your Compose stacks. Every subdirectory inside becomes a stack in Sencho. Falls back to `/app/compose` if unset, but per the [1:1 path rule](#compose-directory-the-11-path-rule) you should always set this explicitly to match the host path you mount. |
- **JWT_SECRET is generated automatically.** Sencho creates a secure random signing key during initial setup and stores it in its database. You do not need to provide one.
+ **The JWT signing secret is generated automatically.** Sencho creates a secure random signing key during initial setup and stores it in its database. There is no environment variable for it: you cannot set or override it, and you do not need to.
### How Sencho organizes your compose directory
@@ -35,6 +35,7 @@ When you point `COMPOSE_DIR` at a directory, Sencho expects each stack to live i
| `SENCHO_USER` | *(unset)* | When set to a username present inside the container (`sencho` is pre-created for this purpose), the entrypoint drops privileges to that user at startup instead of running as `root`. See [Running as a non-root user](#running-as-a-non-root-user) below. |
| `API_RATE_LIMIT` | `200` | Global API requests per minute per user session. Applies in production only; development uses a fixed higher cap. Authenticated requests are keyed by user ID, unauthenticated by IP. Internal node-to-node traffic bypasses this limit. |
| `API_POLLING_RATE_LIMIT` | `300` | Rate limit for dashboard polling endpoints, in requests per minute. Applies in production only; development uses a fixed higher cap. Raise it for environments with many concurrent browser sessions behind shared NAT. |
+| `SENCHO_UPLOAD_DIR` | OS temp dir (`sencho-uploads` subfolder) | Staging directory for file uploads in the Stack file editor. Uploads spool to disk here, outside `COMPOSE_DIR`, before being written to their final location. Set this if your OS temp partition is too small for the uploads you expect. |
## Advanced environment variables
@@ -43,10 +44,14 @@ These tune optional subsystems. Most deployments never set them; the defaults ar
| Variable | Default | Description |
|----------|---------|-------------|
| `TRIVY_BIN` | *(unset)* | Path to a host-installed [Trivy](/operations/trivy-setup) binary for image vulnerability scanning. Sencho prefers a managed install under `DATA_DIR/bin/trivy`, then this path, then `trivy` on `PATH`. |
+| `TRIVY_CACHE_DIR` | `DATA_DIR/trivy-cache` | Directory where Trivy caches its vulnerability database. Move it onto a different volume if `DATA_DIR` is space-constrained. |
| `SENCHO_MESH_SUBNET` | *(auto)* | CIDR for this node's `sencho_mesh` network. When unset, Sencho picks the first free `/24` from its candidate list or adopts an existing mesh subnet. Set one only to avoid an overlap with another network on the host. See [Sencho Mesh](/features/sencho-mesh). |
+| `SENCHO_MESH_RECONCILE_INTERVAL_MS` | `60000` | How often the central instance re-checks proxy-mode mesh tunnels to detect a peer that rebooted. Lower it for faster peer-reboot detection at the cost of more frequent checks. See [Sencho Mesh](/features/sencho-mesh). |
+| `SENCHO_MESH_PROXY_TUNNEL_IDLE_MS` | `0` | Idle timeout before a proxy-mode mesh tunnel tears down and reopens on demand. `0` keeps the tunnel open for the life of the connection. See [Sencho Mesh](/features/sencho-mesh). |
| `GITSOURCE_MAX_CLONE_BYTES` | `104857600` | Maximum bytes a single [Git Source](/features/git-sources) clone may download before it is aborted (100 MB). A shallow Compose clone is tiny; raise it only if you track Compose files in a legitimately large repository. |
| `SENCHO_PUBLIC_URL` | *(request host)* | Set on the primary instance. Its externally reachable `http(s)://` URL, no trailing slash, baked into pilot enrollment so remote agents dial the public hostname rather than the address the admin used at setup. |
-| `SENCHO_COMPOSE_STALL_TIMEOUT_MS` | `600000` | Idle-output backstop for deploy and update Compose steps (pull and recreate). If a step produces no output for this long while still running, Sencho stops it so a hung image pull surfaces a clear failure and the in-app recovery actions instead of spinning. Raise it on slow links or for heavy local image builds. |
+| `SENCHO_COMPOSE_COMMAND_TIMEOUT_MS` | `1800000` | Hard timeout for a single Compose command (pull, up, down) during deploy and update, in milliseconds (30 minutes). Sencho kills the command and reports failure if it runs longer than this, regardless of whether it is still producing output. Raise it only for very large images or slow storage. |
+| `SENCHO_COMPOSE_STALL_TIMEOUT_MS` | `600000` | Idle-output backstop for deploy and update Compose steps (pull and recreate), separate from the hard timeout above. If a step produces no output for this long while still running, Sencho stops it so a hung image pull surfaces a clear failure and the in-app recovery actions instead of spinning. Raise it on slow links or for heavy local image builds. |
| `SENCHO_ZFS_ARCSTATS_PATH` | *(auto)* | Path **inside the container** to the OpenZFS ARC kstat file, for [ZFS ARC-aware host memory](#zfs-arc-aware-host-memory). Sencho checks this path first, then `/host/proc/spl/kstat/zfs/arcstats`, then `/proc/spl/kstat/zfs/arcstats`. Set it only when your ARC stats live at a non-standard path. |
Running a remote host as a pilot agent uses four more variables (`SENCHO_MODE`, `SENCHO_PRIMARY_URL`, `SENCHO_ENROLL_TOKEN`, and `SENCHO_PILOT_CA_FILE`), set only on the remote agent container. Sencho bakes them into the enrollment Compose file it generates, so you rarely write them by hand. See [Pilot Agent](/features/pilot-agent) for the full enrollment walkthrough.
@@ -117,6 +122,7 @@ If you use SSO, configure your identity providers via environment variables:
| `SSO_OIDC_GOOGLE_ENABLED` | Enable Google SSO |
| `SSO_OIDC_GITHUB_ENABLED` | Enable GitHub SSO |
| `SSO_OIDC_OKTA_ENABLED` | Enable Okta SSO |
+| `SSO_OIDC_CUSTOM_ENABLED` | Enable a custom OIDC provider (Keycloak, Authentik, Authelia, Zitadel, and others) |
| `SSO_CALLBACK_URL` | External base URL for OAuth callbacks (required behind reverse proxy) |
For end-to-end provider setup walkthroughs, see the [SSO Quickstart →](/getting-started/sso-quickstart). For the full feature reference, see [SSO Authentication →](/features/sso).
diff --git a/docs/getting-started/introduction.mdx b/docs/getting-started/introduction.mdx
index 50fdbad8..d22f083e 100644
--- a/docs/getting-started/introduction.mdx
+++ b/docs/getting-started/introduction.mdx
@@ -49,10 +49,10 @@ Stacks can be pinned and grouped by label. Each row carries a compact status mar
Opening a stack gives you the day-to-day workspace:
- The identity header shows the node, stack name, runtime state, and main action buttons.
-- Running stacks expose **Restart**, **Stop**, and **Update**; stopped stacks expose **Start** and **Update**. The overflow menu holds less frequent actions such as rollback, config scan, and delete.
+- Running stacks expose **Restart**, **Stop**, **Take down**, and **Update**; stopped stacks expose **Start** and **Update**. The overflow menu holds less frequent actions such as rollback, config scan, and delete.
- Container rows show health, uptime, published ports, live CPU and memory, network activity, logs, and service actions.
- The logs panel can run in **Structured** mode or **Raw terminal** mode.
-- The right panel provides tabs for **Anatomy**, **Activity**, **Dossier**, **Drift**, **Environment**, **Networking**, **Doctor**, and **Storage**, with **Files** and **Edit** controls for browsing stack files and editing compose or env content.
+- The right panel provides tabs for **Anatomy**, **Activity**, **Dossier**, **Drift**, **Environment**, **Compose Labels**, **Networking**, **Doctor**, and **Storage**, with **Files** and **Edit** controls for browsing stack files and editing compose or env content.
## Fleet operations
@@ -64,7 +64,7 @@ The **Fleet** view is the multi-node command center. The masthead summarizes onl
The Fleet toolbar includes **Check Updates**, **Refresh**, and **Add node** for admins. The **Overview** tab supports search, sort, status filters, label filters, and a Grid or Topology view. Node cards show online state, resource use, container counts, version state, update actions, and direct drill-down into stacks on that node.
-Beyond **Overview**, Fleet provides tabs for **Snapshots**, node **Status**, a dependency **Map**, blueprint **Deployments**, **Federation**, and fleet **Actions**. Federation placement (cordon and pin) is available on every tier. Routing and Secrets are limited-availability tabs when enabled on an instance. See [Licensing](/features/licensing) for the full tier breakdown.
+Beyond **Overview**, Fleet provides tabs for **Snapshots**, node **Status**, a dependency **Map**, a **Docker Labels** audit, blueprint **Deployments**, **Federation**, and fleet **Actions**. Federation placement (cordon and pin) is available on every tier. Routing and Secrets are limited-availability tabs when enabled on an instance. See [Licensing](/features/licensing) for the full tier breakdown.
## Resources, templates, and logs
diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx
index 0b9c9d33..2da8778d 100644
--- a/docs/getting-started/quickstart.mdx
+++ b/docs/getting-started/quickstart.mdx
@@ -87,10 +87,10 @@ Open `http://localhost:1852` in a browser. On a fresh install you land on the **
Pick a username, choose a password, confirm it, and click **Initialize console**. The username placeholder shows `admin`. The password must be at least eight characters, and the strength indicator labels the password **Weak**, **Fair**, or **Strong** as you type.
-Sencho then runs a short **environment preflight**: it confirms the Docker engine and Compose plugin are reachable, the compose directory is writable and mounted at a matching host path, the dashboard is behind TLS, and there is disk headroom. Anything that needs attention shows an inline fix. The checks never block you, so click **Enter Sencho** to continue; you can re-run them anytime from **Settings · Recovery**.
+Sencho then runs a short **environment preflight**: it confirms the Docker engine and Compose plugin are reachable, the compose directory is writable, Sencho's own container isn't nested inside that same directory, the host and container paths match, the dashboard is behind TLS, and there is disk headroom. Anything that needs attention shows an inline fix, like the disk space warning below. The checks never block you, so click **Enter Sencho** to continue; you can re-run them anytime from **Settings · Recovery**.
-
+
@@ -99,7 +99,7 @@ Sencho then runs a short **environment preflight**: it confirms the Docker engin
## After signing in
-You land on **Home**, the default operational view. The health masthead reports **Healthy**, **Degraded**, or **Critical**, names the active node, shows how many nodes are registered, and lists any signals that need attention. The resource gauge strip tracks **CPU**, **Memory**, **Disk**, and **Network** with sparklines and threshold coloring. **Stack health** lists the active node's stacks, sorted by state then load, with uptime, CPU, memory, and a 10-minute CPU sparkline per row.
+You land on **Home**, the default operational view. The health masthead reports **Healthy**, **Degraded**, or **Critical**, names the active node, shows how many nodes are registered, and lists any signals that need attention, alongside a running-container count, a live CPU/memory readout, and an alert count. The resource gauge strip tracks **CPU**, **Memory**, **Disk**, and **Network** with sparklines and threshold coloring. **Stack health** lists the active node's stacks, sorted by load by default (click a column header to sort by name, uptime, CPU, or memory instead), with each row's source (**Local** or **Git**), port, uptime, CPU, memory, and a 10-minute CPU sparkline.
@@ -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/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)
- Both files are required for a full restore. If you restore `sencho.db` without the matching `encryption.key`, any encrypted values in the database will be unreadable and you will need to re-enter registry credentials and re-generate node API tokens.
+ Both files are required for a full restore. If you restore `sencho.db` without the matching `encryption.key`, every encrypted value in the database becomes permanently unreadable: registry credentials, remote node tokens, Git source and Fleet Secrets bundles, SSO/MFA configuration, and the compose file contents inside any existing fleet snapshot. There is no way to recover this data without the original key; you would need to re-enter each credential, re-enroll remote nodes, and re-capture any snapshots you rely on.
### 2. Compose directory (`COMPOSE_DIR`)
@@ -57,7 +57,7 @@ docker start sencho
### SQLite online backup (without stopping)
-SQLite supports hot backups via its `.backup` command. This is safe to run while Sencho is running:
+SQLite supports hot backups via its `.backup` command. This is safe to run while Sencho is running, and requires the `sqlite3` CLI to be installed on the host:
```bash
sqlite3 /path/to/sencho-data/sencho.db ".backup '/path/to/backup/sencho.db'"
@@ -65,13 +65,23 @@ sqlite3 /path/to/sencho-data/sencho.db ".backup '/path/to/backup/sencho.db'"
Remember to also copy `encryption.key` from the same directory.
+### Built-in backup command (no host tooling required)
+
+Sencho ships an equivalent online backup as a CLI command that runs inside the container, so you do not need `sqlite3` on the host:
+
+```bash
+docker compose exec sencho node dist/cli/backupData.js [destination-dir]
+```
+
+With no destination argument it writes a timestamped folder under `DATA_DIR/backups`, copies `sencho.db` and `encryption.key` together, and records the action in the audit log. See [Emergency command-line recovery](/operations/emergency-cli#back-up-the-data-directory) for the full command reference.
+
### Automated daily backup (cron example)
```cron
0 3 * * * cp /path/to/sencho-data/encryption.key /backups/encryption.key && sqlite3 /path/to/sencho-data/sencho.db ".backup '/backups/sencho-$(date +\%Y\%m\%d).db'" && find /backups -name "sencho-*.db" -mtime +30 -delete
```
-This copies the encryption key, backs up the database at 3 AM daily, and deletes database backups older than 30 days.
+This copies the encryption key, backs up the database at 3 AM daily, and deletes database backups older than 30 days. If the host does not have `sqlite3` installed, substitute `docker compose exec sencho node dist/cli/backupData.js /backups/sencho-$(date +\%Y\%m\%d)` for the same effect.
---
@@ -130,7 +140,7 @@ docker compose up -d
### Step 4: Update remote node references
-If other Sencho instances were pointing to your old host as a remote node, update their node config to use the new host's IP or hostname. Generate a new API token on the restored instance and distribute it.
+If other Sencho instances were pointing to your old host as a remote node, update their node config to use the new host's IP or hostname. Node tokens are JWTs signed with a secret stored in `sencho.db`, so any token issued before the move is copied along with your data and keeps working automatically; you only need to update the URL, not regenerate the token. Generate a fresh one from **Settings → Infrastructure → Nodes** only if you want to rotate it for its own sake.
### Step 5: Verify
diff --git a/docs/operations/emergency-cli.mdx b/docs/operations/emergency-cli.mdx
index 52179ff6..faecae9a 100644
--- a/docs/operations/emergency-cli.mdx
+++ b/docs/operations/emergency-cli.mdx
@@ -1,7 +1,7 @@
---
title: Emergency command-line recovery
sidebarTitle: Emergency CLI
-description: Host-level commands that recover access when the Sencho UI is unreachable, plus the in-app Recovery surface for everyday health checks.
+description: Host-level commands that recover access when the Sencho UI is unreachable, plus a pointer to the in-app Recovery page for everyday health checks.
---
When the dashboard is unreachable, an identity provider blocks the login screen, or every administrator is locked out, Sencho ships a set of emergency commands you run from a shell on the host. Each one acts on the same SQLite database the application uses (it respects the container's `DATA_DIR`), and each writes an audit-log entry attributed to `cli` where it changes state, so the action is traceable afterwards.
@@ -18,11 +18,11 @@ Each command prints what it did and exits with code `0` on success or non-zero o
Before you rely on Sencho in earnest, back up `DATA_DIR` and `COMPOSE_DIR`. The [Recovery guide](/operations/recovery) explains why most recovery is file-level, and [Backup & Restore](/operations/backup) gives a copy-and-restore routine.
-## In-app Recovery surface
+## The in-app Recovery page
-For everyday checks you do not need a shell. An administrator can open **Settings · Recovery** for a read-only health snapshot: app version, database integrity, encryption-key status, Docker reachability, administrator and two-factor counts, and the non-secret configuration. The page loads without Docker or live metrics, so it stays available when the rest of the dashboard does not. From there you can export the snapshot as JSON, reset this browser's interface preferences to defaults, and see the same command reference printed below.
+For everyday checks you do not need a shell. **Settings · Recovery** is a read-only hub: a System health snapshot (app version, database integrity, encryption-key status, Docker reachability, administrator and two-factor counts, and configured SSO providers, exportable as JSON), the Environment preflight checks, Safe actions such as resetting this browser's interface preferences, and this same command reference with a one-click download. The page loads without Docker or live metrics, so it stays available when the rest of the dashboard does not.
-The command line is the fallback for when even that surface cannot be reached.
+See the [Recovery guide](/operations/recovery) for a full walkthrough of that page and every failure scenario it covers. The command line below is the fallback for when even that page cannot be reached.
## Sign-in and account recovery
@@ -34,7 +34,7 @@ Resets a local user's password and signs out their existing sessions.
docker compose exec sencho node dist/cli/resetPassword.js
```
-Applies to local accounts only. Users who sign in through SSO are managed by their identity provider.
+The new password must be at least 8 characters. Applies to local accounts only; users who sign in through SSO are managed by their identity provider.
### Create an emergency administrator
@@ -44,7 +44,7 @@ Creates a fresh local administrator when every existing admin is locked out but
docker compose exec sencho node dist/cli/createEmergencyAdmin.js
```
-It refuses to overwrite an existing user; use the password reset above for an account that already exists. Sign in afterwards and review your other accounts.
+The username must be at least 3 characters (letters, numbers, underscore, and hyphen only) and the password at least 8. It refuses to overwrite an existing user; use the password reset above for an account that already exists. Sign in afterwards and review your other accounts.
### Clear the existing two-factor enrolment
@@ -74,18 +74,28 @@ Re-enables local password sign-in when a misconfigured identity provider blocks
docker compose exec sencho node dist/cli/disableSso.js [provider]
```
+`provider` is one of the identifiers used in **Settings · SSO** (`ldap`, `oidc_google`, `oidc_github`, `oidc_okta`, or `oidc_custom`), for example:
+
+```bash
+docker compose exec sencho node dist/cli/disableSso.js oidc_google
+```
+
With no argument it disables every enabled provider. The stored configuration is preserved (only the enabled flag is cleared), so you can correct it and turn it back on from **Settings · SSO**.
## Inspecting and protecting your data
### Print a diagnostic summary
-Prints the same snapshot the in-app Recovery surface shows, as JSON. Read-only and free of secrets, so it is safe to paste into a bug report.
+Prints the same snapshot the in-app Recovery page shows, as JSON. Read-only and free of secrets, so it is safe to paste into a bug report.
```bash
docker compose exec sencho node dist/cli/diagnostics.js
```
+
+ The `docker` field always reports `reachable: false` in this command's output: the CLI runs without a live Docker connection, unlike the in-app Recovery page, which probes the socket directly. That does not indicate a problem; check Docker from the in-app page or `docker ps` on the host instead.
+
+
### Validate the database and encryption key
Runs a database integrity check, confirms the core tables exist, confirms the encryption key is present and usable, and confirms at least one administrator exists. Exits non-zero if any check fails.
diff --git a/docs/operations/recovery.mdx b/docs/operations/recovery.mdx
index 30b1e836..d64518ff 100644
--- a/docs/operations/recovery.mdx
+++ b/docs/operations/recovery.mdx
@@ -18,13 +18,26 @@ Your compose files on disk are always the source of truth. Sencho reads them; it
---
+## The in-app Recovery page
+
+Before reaching for a shell, check **Settings · Recovery**. It is a single read-only hub built for exactly the moments this guide covers:
+
+- **System health**: a live snapshot of the database, encryption key, Docker reachability, administrator and two-factor counts, and configured SSO providers, exportable as JSON for a bug report.
+- **Environment**: the preflight checks described below, with a re-run button.
+- **Safe actions**: reset this browser's interface preferences, and a link back to this guide.
+- **Command-line recovery**: every emergency command listed below, with a one-click download of the full reference as a text file, so you have it on hand before you need a shell.
+
+The page loads without Docker or live metrics, so it stays available when the rest of the dashboard does not.
+
## Environment checks
Sencho runs a short environment preflight as the final step of first-boot setup, and you can re-run it anytime from **Settings · Recovery**. It confirms the things a deploy depends on, so a deploy does not fail for an avoidable reason:
- the Docker engine is reachable and Sencho has permission to talk to the socket,
- the Docker Compose plugin is present,
-- the compose directory exists, is writable, and is mounted at a matching host path (the [1:1 path rule](/getting-started/configuration#compose-directory-the-11-path-rule)),
+- the compose directory exists and is writable,
+- Sencho's own compose project is not nested inside `COMPOSE_DIR` (managing itself as a regular stack would create a conflict with self-updates),
+- the compose directory is mounted at a matching host path (the [1:1 path rule](/getting-started/configuration#compose-directory-the-11-path-rule)),
- the dashboard is reached over TLS rather than plain HTTP,
- and the compose volume has disk headroom.
@@ -65,7 +78,7 @@ See [Backup & Restore](/operations/backup#restoring) for the restore steps and [
docker compose -f /path/to/your/stack/compose.yaml logs
```
2. The usual causes are a missing environment variable, a host port already in use, or a bind-mount path that does not exist yet. Each is fixed in the compose file or `.env`.
-3. If a previously working stack broke after an edit, use **one-click rollback** in the editor to return to the last deployed version of the compose file.
+3. If a previously working stack broke after an edit, use **Rollback** from the stack's overflow menu to revert the compose and env files to the last deployed snapshot and redeploy. It is only offered when a pre-deploy backup exists, and it restores the files, not previously built image layers.
See [Troubleshooting: containers won't start after deploy](/operations/troubleshooting#containers-wont-start-after-deploy) for the full cause list and [Stack management](/features/stack-management) for the editor and rollback.
@@ -73,14 +86,15 @@ See [Troubleshooting: containers won't start after deploy](/operations/troublesh
## You are locked out of sign-in
-**Symptom:** An administrator has lost their second factor, or the admin password is forgotten, and no one can sign in through the UI.
+**Symptom:** An administrator has lost their second factor, the admin password is forgotten, or a misconfigured identity provider blocks the login screen entirely, and no one can sign in through the UI.
**What to do, least disruptive first:**
-1. **Another admin is still in:** any administrator can reset a locked-out user's two-factor enrolment from **Settings · Users**. The user then signs in with their password alone and re-enrols.
-2. **Every admin has lost 2FA:** reset two-factor for the admin account directly on the host with the emergency command-line reset. It acts on the same database the app uses and records the action in the audit log.
-3. **The admin password itself is forgotten:** Sencho has no password-reset email flow, but you do not need to discard your configuration. From a shell on the host, reset the password with the emergency command line, or create a fresh emergency administrator. Both act on the live database and leave your nodes, alert rules, and labels untouched.
-4. **Last resort, the database is unusable:** reset first-boot setup by removing `sencho.db`, which lets you create a fresh admin account from scratch.
+1. **A misconfigured SSO/OIDC/LDAP provider blocks the login screen:** disable it from the host with the emergency command line. The stored configuration is preserved, only the enabled flag is cleared, so local password sign-in becomes reachable again and you can correct and re-enable the provider from **Settings · SSO**.
+2. **Another admin is still in:** any administrator can reset a locked-out user's two-factor enrolment from **Settings · Users**. The user then signs in with their password alone and re-enrols.
+3. **Every admin has lost 2FA:** reset two-factor for the admin account directly on the host with the emergency command-line reset. It acts on the same database the app uses and records the action in the audit log.
+4. **The admin password itself is forgotten:** Sencho has no password-reset email flow, but you do not need to discard your configuration. From a shell on the host, reset the password with the emergency command line, or create a fresh emergency administrator. Both act on the live database and leave your nodes, alert rules, and labels untouched.
+5. **Last resort, the database is unusable:** reset first-boot setup by removing `sencho.db`, which lets you create a fresh admin account from scratch.
Removing `sencho.db` resets all Sencho configuration: users, nodes, alert rules, notification settings, and labels. Your Docker stacks and their compose files are not affected, and Sencho re-discovers them on the next scan. Use this only when no administrator can sign in and the command-line recovery above is not enough.
diff --git a/docs/operations/trivy-setup.mdx b/docs/operations/trivy-setup.mdx
index 7cb5b53b..25a9d133 100644
--- a/docs/operations/trivy-setup.mdx
+++ b/docs/operations/trivy-setup.mdx
@@ -11,6 +11,10 @@ Sencho's [Vulnerability Scanning](/features/vulnerability-scanning) feature uses
Once Trivy is available through any of these options, the scanning UI appears automatically.
+
+Trivy is installed independently on each node. In a fleet, the Scanner setup tab manages the scanner for whichever node is active, so repeat one of the options above on every node you want to scan images on.
+
+
## Why Trivy is not bundled
Trivy's vulnerability database updates multiple times per day and is around 100 MB. Bundling Trivy would force every Sencho instance to carry an out-of-date database in its image, then re-download on first scan. Keeping Trivy external lets you:
@@ -28,7 +32,7 @@ Sencho can install and manage Trivy for you without any extra bind mounts or env
3. Wait for the status to flip to **Installed (managed)**. The version appears next to the badge.
-
+
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.
+
+ The shield icon is not hidden on your own row. If you use it on yourself, it signs you out immediately, the same as it does for any other user, and you sign back in with your password alone. If you still have a working authenticator or a backup code, disabling from **Settings · Account · Two-factor authentication** instead keeps your session alive, since that flow proves possession first and reseals your session under the new token version.
+
+
+## Lockout after failed attempts
+
+A reset is also the fix for a user locked out by repeated bad codes, not just a lost authenticator. Five wrong TOTP or backup-code submissions in a row during sign-in lock the account for 15 minutes. The lockout blocks further sign-in attempts only; any session the user already has open keeps working.
+
+Waiting out the 15 minutes clears the lockout on its own. If the user needs back in sooner, or you are not sure whether they are locked out or genuinely lost their authenticator, an administrator reset from **Settings · Users** clears both: it removes the enrolment entirely, which also drops any in-progress lockout.
+
## Emergency recovery from the command line
If every administrator has lost access to 2FA and no one can sign in through the UI, reset the admin account directly on the host running Sencho.
diff --git a/docs/operations/upgrade.mdx b/docs/operations/upgrade.mdx
index 1bf57e1c..4d7f4ddc 100644
--- a/docs/operations/upgrade.mdx
+++ b/docs/operations/upgrade.mdx
@@ -24,10 +24,14 @@ docker stop sencho && docker rm sencho
# Re-run your original docker run command
```
-Sencho will apply any necessary database migrations automatically on startup. No manual steps are required.
+
+ Sencho is also mirrored on GitHub Container Registry at `ghcr.io/studio-saelix/sencho:latest` with the same tags and digests as Docker Hub. Pull from whichever registry your environment prefers.
+
+
+Sencho applies any necessary database migrations automatically on startup. No manual steps are required.
- Remote nodes can be updated directly from the Fleet View without dropping to a shell. See [Remote Updates](/features/remote-updates) for details.
+ Remote nodes can be updated directly from the Fleet View without dropping to a shell. See [Remote Updates](/features/remote-updates) for details. Admiral **Hardened Build** installs use a different, entitlement-gated update path instead of the manual steps above; see [Updating the local (gateway) node](/features/remote-updates#updating-the-local-gateway-node) for that flow.
---
@@ -37,11 +41,11 @@ Sencho will apply any necessary database migrations automatically on startup. No
Sencho handles all schema changes internally. When the application starts, it checks whether the database needs updating and applies any pending migrations automatically. This covers:
- **New tables and columns** for features introduced in newer versions
-- **Legacy config migration** from very early versions that used a JSON config file
-- **Admin account migration** from single-admin storage to the multi-user system
-- **Encryption of sensitive values** (node API tokens, registry credentials) that were previously stored in plaintext
+- **Config import** from an on-disk JSON config file, if one exists from a manual setup
+- **Admin account consolidation** into the multi-user system
+- **Encryption of node API tokens** stored in plaintext (registry credentials are always stored encrypted, so they need no migration)
- **SSO and RBAC setup** for single sign-on provider config and role-based access
-- **Legacy cleanup** of obsolete fields from pre-0.7 versions (SSH/TLS columns)
+- **Obsolete column cleanup**, removing unused SSH/TLS configuration columns no longer read or written by the current schema
- **Blueprint rollout approval** columns (`approval_status`, intent fingerprint, and blast JSON). Existing Blueprints start **pending**. Automatic reconciliation and enforcement pause until an admin opens **Apply now**, reviews the rollout preview, and confirms. New installs behave the same way for each Blueprint until the first confirmation.
You never need to run SQL commands, migration scripts, or any manual database operations.
@@ -67,12 +71,10 @@ See the [Backup & Restore guide](/operations/backup) for full backup procedures.
If you prefer to control exactly which version you run, pin the image tag in your `docker-compose.yml`:
```yaml
-image: saelix/sencho:0.38.0
+image: saelix/sencho:0.96.0
```
-
-
-Fleet can update semver pins directly from the Node updates sheet: it rewrites the tag to the selected release before recreating the container. Digest pins and compose values that use variable interpolation must be changed manually before Fleet can update the node.
+Fleet can update semver pins directly from the [Node updates](/features/remote-updates) sheet: it rewrites the tag to the selected release before recreating the container. Digest pins and compose values that use variable interpolation must be changed manually before Fleet can update the node.
Check [GitHub Releases](https://github.com/studio-saelix/sencho/releases) for available versions and changelogs.
diff --git a/docs/operations/verifying-images.mdx b/docs/operations/verifying-images.mdx
index e1831354..7d94ace6 100644
--- a/docs/operations/verifying-images.mdx
+++ b/docs/operations/verifying-images.mdx
@@ -131,6 +131,19 @@ Each release publishes two moving tags plus one immutable tag, so you can choose
For production, pin to `X.Y.Z` or `X.Y` and verify the signature on every pull. For staging or development, `latest` is fine.
+### Integration tag (post-merge, pre-release)
+
+Every push to `main` publishes the current integration build so you can pull and smoke-test the exact code queued for the next release before it is ever tagged. This tag is GHCR-only, published to a separate package from the release image, and carries no signature, SBOM, or VEX attestation.
+
+| Tag | Example | Updates on |
+|---|---|---|
+| `dev` | `ghcr.io/studio-saelix/sencho-dev:dev` | Every push to `main` |
+| `dev-` | `ghcr.io/studio-saelix/sencho-dev:dev-a1b2c3d` | Never (immutable per build) |
+
+
+The `:dev` image is not for production. It is unsigned, is never published to Docker Hub, and does not carry the SBOM, provenance, or VEX attestations described above.
+
+
### Preview tags (pre-merge only)
Maintainers publish these from open PRs for external validation. They are unsigned and not for production.
diff --git a/docs/reference/contact.mdx b/docs/reference/contact.mdx
index 7afb6f83..da79ba9b 100644
--- a/docs/reference/contact.mdx
+++ b/docs/reference/contact.mdx
@@ -9,37 +9,40 @@ Sencho provides dedicated email channels for different types of inquiries. Use t
## Support
- Technical support, how-to questions, and troubleshooting help. Available to Admiral license holders via **Settings > Help & Support** in the app.
+ Priority email support, reserved for Admiral license holders: Monday to Friday, 09:00-17:00 America/New_York, with a one-business-day first-response target. This is not a contractual SLA or 24/7 service.
-Community users can get help through the [Documentation](https://docs.sencho.io) and [GitHub Issues](https://github.com/studio-saelix/sencho/issues).
+Every user, Community and Admiral alike, can reach [Documentation](https://docs.sencho.io) and [GitHub Issues](https://github.com/studio-saelix/sencho/issues) from **Settings → Help → Support** in the app. Admiral adds the priority email channel above on the same page.
## General inquiries
-
- General inquiries, partnership proposals, and media requests. Found in the website footer under **Community > Contact**.
+
+ General inquiries, partnership proposals, media requests, and Admiral pricing conversations.
+Found in the website footer under **Community > Contact**, and behind the **Get in touch** button on the [pricing page](https://sencho.io/pricing). Admiral is arranged directly with Studio Saelix rather than a self-serve checkout, so this is also the address to start a pricing or trial conversation.
+
## Licensing
- License activation issues, upgrade questions, custom pricing, refund requests, and alternative licensing arrangements.
+ Activation issues, billing questions, and refund requests for an Admiral license you already hold.
This address appears in:
-- The [LICENSE](https://github.com/studio-saelix/sencho/blob/main/LICENSE) file for alternative licensing inquiries
-- Upgrade prompts inside the app (Admiral plan callouts)
-- The [Refund Policy](https://sencho.io/refund) and pricing section on the website
+- The [Refund Policy](https://sencho.io/refund), for refund requests (include your license key or order number)
+- [TRADEMARKS.md](https://github.com/studio-saelix/sencho/blob/main/TRADEMARKS.md), for trademark questions
+
+For a new Admiral conversation (pricing, trials, procurement), use **hello@sencho.io** above instead.
## Security
- Vulnerability reports, security incidents, and bug bounty submissions. **Do not open a public issue for security vulnerabilities.**
+ Vulnerability reports and security incidents. **Do not open a public issue for security vulnerabilities.**
-You can also use [GitHub private vulnerability reporting](https://github.com/studio-saelix/sencho/security/advisories/new). Both the app and the website publish a `/.well-known/security.txt` file that points to this address.
+You can also use [GitHub private vulnerability reporting](https://github.com/studio-saelix/sencho/security/advisories/new). Both the app and the website publish a `/.well-known/security.txt` file that points to this address. Expect an initial response within 72 hours.
-See the full [Security Policy](https://github.com/studio-saelix/sencho/blob/main/SECURITY.md) for response times and disclosure guidelines.
+See the full [Security Policy](https://github.com/studio-saelix/sencho/blob/main/SECURITY.md) for supported versions and disclosure guidelines.
## Privacy
@@ -61,9 +64,9 @@ All reports are reviewed promptly and handled with confidentiality. See the [Cod
| Email | When to use |
|-------|-------------|
-| `support@sencho.io` | Technical help (Admiral) |
-| `contact@sencho.io` | General inquiries, partnerships, media |
-| `licensing@sencho.io` | Upgrades, custom deals, refunds, license issues |
+| `support@sencho.io` | Priority technical support (Admiral only) |
+| `hello@sencho.io` | General inquiries, partnerships, media, new Admiral conversations |
+| `licensing@sencho.io` | Activation, billing, and refunds for an existing license |
| `security@sencho.io` | Vulnerability reports, security incidents |
| `privacy@sencho.io` | GDPR/CCPA requests, legal, DMCA |
| `conduct@sencho.io` | Code of Conduct violations |
diff --git a/docs/reference/security.mdx b/docs/reference/security.mdx
index e32b8b4f..b8d0a8d9 100644
--- a/docs/reference/security.mdx
+++ b/docs/reference/security.mdx
@@ -101,6 +101,8 @@ Every Sencho instance includes the foundational security stack. Advanced access-
Sencho enforces a minimum 8-character password policy aligned with [NIST SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html). Passwords are hashed with bcrypt (cost factor 10) before storage. On first launch, you create the initial admin account through a setup wizard; additional users can be created from **Settings · Users**.
+The password field in **Settings · Account** shows a live strength indicator that recommends 12 or more characters with mixed case and a number, but this is guidance only: any password meeting the 8-character minimum is accepted.
+
Changing your password immediately invalidates all other active sessions, so a compromised session cannot survive a password reset.
## Single sign-on
@@ -162,7 +164,7 @@ API tokens provide scoped, long-lived access for CI/CD pipelines and automation
| **Deploy Only** | Everything in Read Only, plus deploy, down, restart, stop, start, and update |
| **Full Admin** | All API operations |
-Regardless of scope, tokens are universally blocked from sensitive operations: password management, user administration, SSO configuration, node management, license management, token management, registry credentials, and console access.
+Regardless of scope, tokens are universally blocked from sensitive operations: password and MFA management, user administration, SSO configuration, node management, license management, token management, registry credentials, console access, Recovery Vault, and image channel management.
Tokens are cryptographically hashed before storage; the raw value is shown exactly once at creation. Each token tracks its last-used timestamp for auditing. You can configure expiry at 30, 60, 90, or 365 days, or leave it open-ended. A maximum of 25 active tokens are allowed per user.
@@ -230,6 +232,7 @@ Sencho encrypts all sensitive values with AES-256-GCM before writing them to the
- Remote node API tokens
- Registry credentials (private image registries)
- Fleet Secrets bundle payloads (env-var key/value pairs)
+- Recovery Vault credentials (S3-compatible secret key)
A single 32-byte data key seals all of the above. The key is generated automatically on first startup and stored in a dedicated file with 0600 permissions (owner-only read/write). If the file permissions are found to be too permissive at startup, Sencho corrects them automatically.
diff --git a/docs/reference/settings.mdx b/docs/reference/settings.mdx
index a7aae0bb..6bde31c4 100644
--- a/docs/reference/settings.mdx
+++ b/docs/reference/settings.mdx
@@ -15,10 +15,10 @@ Open the Settings Hub by clicking the **Profile** icon in the top bar and select
| Group | What it covers |
|-------|----------------|
| **Personal** | Account, Appearance |
-| **Access** | License, Users, SSO, API Tokens |
-| **Infrastructure** | Nodes, Fleet, Registries, Recovery Vault, App Store, Stacks |
+| **Access** | Admiral Account, Users, SSO, API Tokens |
+| **Infrastructure** | Nodes, Stacks, Fleet, Registries, Recovery Vault, App Store |
| **Monitoring** | Host Alerts, Container Alerts, Docker & Storage |
-| **Notifications** | Channels, Notification Routing |
+| **Notifications** | Channels, Notification Routing, Mute Rules |
| **Automation** | Image update checks, Webhooks |
| **Organization** | Labels |
| **Operations** | Data Retention, Developer Diagnostics, Recovery |
@@ -37,7 +37,7 @@ Every section renders inside the same masthead-and-sidebar layout. The masthead
| **SCOPE** `operator` / `browser` / `global` | Setting applies to your account (`operator`), to this browser only (`browser`, for browser-local sections such as Appearance), or to the whole instance (`global`, every other non-node group) |
| **NODE** `` | Setting is per-node and is currently being edited against this node |
| **EDITED** `` pending / `saved` | The current section has unsaved changes |
-| Section-specific stats | Each section can publish its own pills: `2FA on`/`off` and `BACKUP left` (Account); `PLAN`, `TRIAL d left`, `RENEWS`, `STATUS` (Admiral Account); `OPERATORS` (Users); `CHANNELS` (Channels); `ROUTES` (Notification Routing); `WEBHOOKS` and `ENABLED` (Webhooks); `LABELS` (Labels); `PROVIDER`, `USED`, `SNAPSHOTS` (Recovery Vault); `DEV MODE` (Developer Diagnostics) |
+| Section-specific stats | Each section can publish its own pills: `2FA on`/`off` and `BACKUP left` (Account); `PLAN`, `DURATION`, `TRIAL d left`, `RENEWS`, `STATUS` (Admiral Account); `OPERATORS` (Users); `PROVIDERS`, `ENABLED` (SSO); `CHANNELS` (Channels); `ROUTES`, `ENABLED` (Notification Routing); `WEBHOOKS` and `ENABLED` (Webhooks); `LABELS` (Labels); `PROVIDER`, `USED` (Recovery Vault); `DEV MODE` (Developer Diagnostics) |
### Quick search
@@ -74,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
@@ -126,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. |
@@ -162,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.
---
@@ -297,11 +314,15 @@ Click **Save settings** to apply.
**Scope:** Per-node (applies to the currently selected node)
-
+
### Mesh data plane
+
+ This subsection is not on every installation. It appears once mesh discovery is turned on for this control plane.
+
+
| Setting | Default | Description |
|---------|---------|-------------|
| **Auto-recreate mesh network** | Off | If `sencho_mesh` is removed at runtime, rebuild it at the same subnet on the next 10-second tick. Off by default; leave it off and restart Sencho manually for the safest path. See [Sencho Mesh](/features/sencho-mesh) for the reconciler behavior. |
@@ -338,7 +359,7 @@ See [Private Registries](/features/private-registries) for the full walkthrough.
**Scope:** Global, admin-only
-Mirror fleet snapshots to Recovery Vault or any S3-compatible storage so a control-plane loss does not take the recovery history with it. The masthead publishes a **PROVIDER** pill (`sencho` / `s3` / `disabled`), a **USED** pill showing storage consumption, and a **SNAPSHOTS** pill with the cloud object count.
+Mirror fleet snapshots to Recovery Vault or any S3-compatible storage so a control-plane loss does not take the recovery history with it. The masthead publishes a **PROVIDER** pill (`sencho` / `s3` / `disabled`) and a **USED** pill showing storage consumption; the object count appears inline in the Storage used row.
@@ -517,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.
@@ -704,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.