* feat: add notification suppression rules
* fix: restore label routing and routing test mocks for suppression
* fix: allow bell mute shortcuts for history-only notification categories
Suppression rule validation used the routable category whitelist, which rejected history-only categories such as update_started that appear in the bell during stack updates.
* feat: expand Mute Rules UX with compose-first entry points and activity badges
* fix: add missing NodeContext mocks for notification suppression tests
The fleet overview, configuration, dependency-map, and networking-summary reads
were authentication-only, so a role without node:read (deployer) could read node
names, host stats, and cross-node topology. They now require node:read, matching
the role model where every role except deployer holds it.
For parity, the Fleet nav entry is gated on node:read (hiding it from the top
nav, mobile menu, and command palette), the Fleet view redirects to the
dashboard when reached without it, and the dashboard fleet heartbeat falls back
to the single-node restart map for a role that cannot read fleet data.
A batch of UI/UX polish:
- New independent "Reduced motion" appearance setting (separate from Reduced
effects). Drives framer-motion via MotionConfig and clamps CSS transitions via
data-motion on <html>; toasts are unaffected. Defaults off (OS preference
still honored).
- Stack-detail Files tab: rename "Files & Volumes" to "Files", add a persisted
word-wrap toggle to the file viewer (default on), and add a fullscreen toggle
that collapses the Command Center + Logs column so the editor fills the width.
- Create Stack > From Git: remove the nested scroll clamp so the deploy toggle
and footer are reachable.
- Fleet: full-width tab band with icon-only Refresh / Export Dossier, icon-only
Check-for-updates / Add-node on the Overview toolbar, theme-aware empty-state
headings (calm drops the italic), and fix the Actions card body overlapping
the action-row divider.
- Snapshots: restyle Restore and Restore all to the ghost button design used by
View / Preview / Download, and right-align the per-stack Restore.
- Settings sidebar: App Store gradient active style and standard font size.
- Compose Doctor: dismiss the high-risk banner (and clear the tab dot) until the
findings change, via a shared fingerprint-keyed hook.
- Stack-detail Storage: link the "no recent fleet snapshot" warning to the Fleet
Snapshots tab (FleetView tabs are now controlled to support the deep link).
* feat: chart-led Security overview with sortable Images and History tables
Refine the Security page around the existing design system and add the
data the dashboard needs.
- Overview leads with four charts (30-day risk trend, severity donut, top
exposed images, findings by type); the signal-rail counts become a
secondary summary, and the scanner and deploy-enforcement posture follow.
- Images becomes a recessed table with search, a severity filter, sortable
columns, a last-scan column, and inline scan actions; the findings cell is
clickable into the scan sheet, and the per-row cursor tooltip is dropped
where the columns already carry that information.
- Policies puts deploy-enforcement first, collapses the policy packs into an
accordion, and uses the standard primary button for Add policy.
- Suppressions and acknowledgements move their titles and Add buttons outside
the cards, matching the Fleet tab layout.
- History switches from the detail sheet to an inline table (search, sortable
columns, two-scan compare, pagination); the now-unreachable scan-history
overlay is removed.
- Add GET /api/security/overview/trend, a node-scoped daily critical/high
rollup backing the risk-trend chart.
- Extract the shared image-scan hook and the severity classifier, and harden
the overview data fetch so a malformed non-critical response can never read
as a clean security state.
* fix: treat malformed Security responses as errors, not empty or clean states
Address an independent review of the data-fetch paths so a 200 with an
unexpected shape can never read as a benign "no findings" view.
- SecurityView: validate that the image-summaries body is a scan-summary map; an
unexpected shape now sets the error state instead of an empty map. Isolate the
trend fetch in its own self-catching promise so a transport failure on the
non-critical chart can no longer poison the overview or summaries error state.
- useImageScan: only a "completed" poll counts as success (a malformed or unknown
status now throws), and a failed post-scan summaries refresh is logged instead
of silently dropped.
- HistoryTab: a 200 whose body lacks an items array is treated as an error, not
an empty "no completed scans" list.
* feat: add a compact icon-only top navigation toggle
Add a browser-local "Top navigation labels" preference under Settings >
Appearance. With it off, the desktop top navigation renders icon-only;
each destination keeps an aria-label, gains a hover/focus tooltip, and
stays reachable from the command palette. The setting defaults on, so
current behavior is preserved, and the mobile navigation always keeps
its labels.
Also left-align the desktop nav (previously centered) and shorten the
longest nav label from "Auto-Update" to "Update" so the bar scans faster.
* feat: let the icon-only top nav be left or centered
Add a "Top navigation alignment" preference under Settings > Appearance
that appears only when top navigation labels are off. It places the
icon-only bar against the left edge (the default) or centered. With
labels on, the nav always stays left so the longer labels read from the
edge. The choice is browser-local and persists per device.
* feat: add dedicated Security page and policy-pack foundation
Bring vulnerability scanning, scan history, suppressions, Compose risks,
secrets, policy packs, and scanner setup into one node-scoped Security
command center instead of scattering them across Resources and Settings.
- New top-level Security view with Overview, Images, Compose risks,
Secrets, Policies, Suppressions, History, and Scanner setup tabs
(status masthead + signal rail; controlled tabs with deep-link support).
- Backend: GET /security/overview rollup and GET /security/policy-packs
static catalog (auth-only, Community). DatabaseService gains an uncapped
scan-status count and a node-eligible block-policy count, and
getImageScanSummaries now projects secret and misconfig counts.
- Reuse existing surfaces: the scan-history sheet, the control-governed
suppression and acknowledgement panels, and the scan-detail sheet (now
with an initial-tab prop so it opens on the matching finding type).
- Extract a shared SeverityBadge (from Resources) and a TrivyManager
(from Settings) so both surfaces render identical controls.
- Resources "Scan history" now links into the Security page History tab.
- Docs for the new Security surface and tests for the new endpoints,
helpers, nav wiring, and tabs.
* refactor: consolidate scanner and policy management onto the Security page
Remove the Settings "Vulnerability Scanning" section now that the Security
page covers the same ground, with every option preserved:
- Scanner install / update / uninstall / auto-update live on the Scanner setup
tab (TrivyManager).
- Scan policies, the honor-suppressions toggle, and the replica
managed-by-control / demote controls move into a new ScanPolicyManager on the
Policies tab (paid; Community sees only the policy-pack catalog).
- CVE suppressions and acknowledgements remain on the Suppressions tab.
Wiring removed: the registry section and the now-empty Security settings group,
the SectionId, the SettingsSectionContent case and the isPaid prop it was the
sole consumer of, and SecuritySection itself. The dashboard configuration-status
"Vulnerability scanning" row now navigates to the Security page Policies tab.
Docs that pointed at "Settings -> Security -> Vulnerability Scanning" are swept
to the relevant Security page tabs.
* fix: harden Security page scanner refresh, policy-load errors, and secret-only badges
Address independent-review findings on the Security page:
- Scanner setup now refreshes Trivy state when the active node changes, so the
displayed scanner status matches the node TrivyManager's actions target (both
follow x-node-id). Previously, switching nodes on the tab left stale state.
- ScanPolicyManager surfaces an explicit error state on a failed policy fetch
instead of falling through to a false "No scan policies configured".
- The shared SeverityBadge and the Images findings column no longer label a scan
"clean" when it has secrets or misconfigurations but no CVE severity
(highest_severity is derived from vulnerabilities only); they show a "Findings"
state and the secret/misconfig counts instead.
- The Overview enforcement note points to the Policies tab, not the removed
Settings section.
- The History tab auto-opens the scan-history sheet only on a deep-link (mount
with the History tab active), not on every manual tab selection.
Adds tests for the badge secret/misconfig state and the policy-load error state.
* feat(pricing): collapse to two tiers (Community + Admiral)
Collapse Sencho's pricing from three tiers (Community / Skipper / Admiral)
to two: a generous free Community tier and a single paid Admiral tier. The
Skipper tier is removed.
Now free in Community: auto-heal, auto-update, scheduled operations,
webhooks, notification routing, Fleet Actions and bulk operations, SSO
preset providers (Google / GitHub / Okta), unlimited users with admin and
viewer roles, and deploy safety (atomic deploys, auto-rollback, and
one-click rollback).
Admiral (paid) is focused on running and governing a fleet: blueprints,
Fleet Secrets, deploy enforcement, vulnerability report export, audit log,
host console, private registries, mesh networking, node cordon, managed
cloud backup, LDAP / Active Directory SSO, and the advanced RBAC roles
(deployer, node-admin, auditor) with per-resource scoped assignments.
Internally the license variant distinction is removed so tier is binary
(community / paid). License validation still verifies the Lemon Squeezy
store and product before granting paid status.
Docs and the contributor guide are updated to the two-tier model.
* docs(pricing): correct licensing page to two-tier pricing and tidy stale tier wording
The licensing docs page kept the old Admiral pricing plus a Founder
Lifetime column and an Enterprise paragraph after the two-tier collapse.
Update it to $12/month or $99/year, drop the lifetime and Enterprise
content, and link to the pricing page for current pricing.
Also fix stale "Skipper" wording in CLA.md, SUPPORT.md, one test title,
and three test comments. Historical CHANGELOG entries and the
retired-Skipper license-guard test are intentionally left as-is.
* docs: align licensing and SSO pages with the two-tier model
Correct the SSO overview so the Google, GitHub, and Okta presets read as
available on every tier, matching the provider table; only LDAP and Active
Directory require Sencho Admiral. Remove the lifetime-plan references from the
licensing, settings, and troubleshooting pages so they reflect subscription-only
Admiral pricing.
* fix(rbac): omit scoped permissions from /me on the Community tier
Scoped role assignments only take effect on the paid tier, but GET /api/permissions/me returned them unconditionally, so a downgraded instance with leftover assignments rendered per-resource affordances the API then rejected with 403. The endpoint now mirrors the permission middleware and includes scoped permissions only on the paid tier. Adds a regression test covering the downgrade case.
* docs: use custom-pricing wording on the contact page
The two-tier model has no Enterprise tier; reword the contact page's enterprise pricing/deals to custom pricing/deals so it does not imply a tier that no longer exists.
* fix(observability): gate global logs to admins, scope to managed containers, harden SSE
Make the Logs feed an administrator view enforced on both sides (requireAdmin on
the /api/logs/global poll and SSE routes; the Logs nav item plus a redirect guard
on the frontend), and scope the feed to Sencho-managed containers only via a
shared isManagedByComposeDir helper that /stats now reuses.
Harden the SSE stream: a stateful frame demuxer that survives chunk boundaries so
a Docker frame split across reads is reassembled instead of dropped or garbled; a
per-stream error listener so one broken follow stream cannot crash the event loop
(it posts a single degraded notice and keeps the others alive); a cap on
concurrent follow streams with a truncation notice; a bounded initial tail; and
backpressure that pauses the source streams when the client is slow and resumes on
drain. Bound the polling snapshot's per-container fan-out with a concurrency limit.
Add process-local, in-memory log-stream counters exposed at the admin-only
/api/system/log-stream-metrics endpoint (active connections, lines streamed,
attach and frame errors). Collapse the view to the local hub and remove the dead
remote-node handling.
* fix(observability): close remote-proxy bypass of the global-logs admin gate
The logs feed's requireAdmin lives in the local route handler, which the remote
proxy skips when forwarding a request whose nodeId targets a remote node. A hub
user could therefore request /api/logs/global*, /api/logs/global/stream, or
/api/system/log-stream-metrics with x-node-id (or ?nodeId= for the SSE transport)
pointing at a remote node and have it served as the node-proxy admin on the far
side, sidestepping the gate entirely.
Add these paths to HUB_ONLY_PREFIXES so hubOnlyGuard rejects a remote nodeId with
403 before the proxy runs, matching the existing protection on audit-log,
scheduled-tasks, and notification-routes. Add regression tests covering the
collection path, the SSE sub-path (both the x-node-id header and the ?nodeId=
query transport), and the stream-metrics endpoint.
The Schedules sidebar entry was hidden from Skipper admins even though the
backend permits them to create and run update, scan, and snapshot schedules.
The action picker also showed all 10 actions to every paid admin, so a Skipper
selecting Restart, Prune, or any auto_* lifecycle action would 403 on submit.
Changes:
- Extract SKIPPER_SCHEDULED_ACTIONS as the single source of truth in
tierGates.ts; both requireScheduledTaskTier and the GET /scheduled-tasks
list filter now reference it (replaces a duplicate local constant in
scheduledTasks.ts).
- Move the Schedules nav entry from the Admiral block into the
isPaid && isAdmin block in useViewNavigationState.ts, mirroring the
existing Auto-Update pattern. Console and Audit stay Admiral-only.
- Filter the create-form action picker in ScheduledOperationsView.tsx by
license variant. Skipper sees Auto-update Stack, Auto-update All Stacks,
Fleet Snapshot, and Vulnerability Scan; Admiral sees the full set.
- openCreate now defaults formAction to the first visible option so Skipper
starts with a valid choice instead of the Admiral-only Restart.
Tests:
- Add Skipper-variant POST coverage in scheduled-tasks-routes.test.ts:
three allow cases (update / scan / snapshot) and a six-action rejection
loop covering restart / prune / auto_backup / auto_stop / auto_down /
auto_start.
- Flip the Skipper assertion in useViewNavigationState.test.tsx to expect
scheduled-ops alongside auto-updates.
* feat(nodes): hide hub-only views when active node is remote
Fleet, Schedules, Audit, Logs, and Auto-Update operate on hub-owned state
(node registry, fleet schedules, centralized audit, fleet-wide log
aggregation, fleet-wide update preview). When the active node is remote,
proxying those surfaces would show that remote's own disconnected state
instead of the hub's. Hide them from the nav strip and force-redirect to
Home if one was open during the node switch.
Backend hubOnlyGuard middleware sits between nodeContextMiddleware and the
remote proxy and rejects /api/scheduled-tasks, /api/audit-log, and
/api/notification-routes with 403 + HUB_ONLY_ENDPOINT when nodeId resolves
to a remote, closing the script-bypass path the UI gating cannot reach.
Settings sub-sections were already gated via the hiddenOnRemote registry;
this extends the same model to top-level views.
* docs(nodes): note hub-only visibility on Fleet, Schedules, Audit, Logs, Auto-Update
Each of the five hub-only feature pages now points readers to the
canonical "What top-level views show when a remote node is active"
section in multi-node.mdx, so users landing directly on a feature page
understand why the nav item disappears when they switch to a remote node.