Commit Graph

9 Commits

Author SHA1 Message Date
Anso d9b7911f12 fix: distinguish failed image-update checks from "up to date" (#1470)
* fix: distinguish failed image-update checks from "up to date"

The image-update detector collapsed every failure (registry unreachable,
missing auth, rate limit, unresolved local digest) into hasUpdate:false and
dropped the captured reason, so a failed check was indistinguishable from a
current image and never raised a notification, even while a manual stack
update still pulled a newer image.

Detection now records a tri-state per stack (ok / partial / failed) with the
failure reason, exposed via a new GET /api/image-updates/detail (the boolean
GET / is unchanged so fleet aggregation is unaffected). A fully-failed check
preserves the last known has_update, so a transient outage neither erases a
real update nor flaps the notification state. The sidebar shows a muted
"couldn't check" indicator with the reason on hover, and the Update board
lists stacks whose check failed in a "could not be checked" advisory.

Detector hardening: the manifest digest lookup issues HEAD first (falling back
to GET) so it no longer draws down Docker Hub's anonymous pull-rate budget, and
local RepoDigest matching is normalized so official library/* images resolve
their digest instead of falling through to a silent "no update".

* fix: preserve confirmed updates through partial checks; tighten failure surfacing

Address review findings on the tri-state image-update detection:

- A partial check (some images errored) no longer erases a previously
  confirmed update; only a fully-ok check can lower has_update, so a single
  image's registry blip cannot drop the stack's update and re-fire the
  notification on recovery. Adds a regression test.
- The image-level catch stores getErrorMessage(e) rather than raw String(e),
  since that value surfaces verbatim in the sidebar tooltip and readiness
  advisory.
- useImageUpdates and the readiness detail fetch now log unexpected non-ok
  responses instead of silently leaving stale state.
- Remove an unused checkFailedCount derivation (the row indicator is driven by
  the checkStatus prop).
- Reword the recordStackCheckFailure docstring and the HEAD-first comment.
2026-06-26 16:16:34 -04:00
Anso ca496c89dc fix: name matched risk inputs in policy block messages (#1471)
The auto-update, bulk-label, scheduler, and blueprint deploy block
messages hardcoded "image(s) exceed <max_severity>", which is wrong
under the risk-first policy model: a block can be driven by a
known-exploited (KEV) or fixable Critical/High input while the severity
threshold was never the trigger. In those cases the message named a
severity ceiling the policy did not enforce.

Route all four message paths through a shared summarizeBlockReasons
helper (the same reason text the deploy-gate 409 response and the block
dialog already use), so every surface names the inputs that actually
matched. Falls back to a generic phrase when no reason was recorded.
2026-06-26 15:34:24 -04:00
Anso 7320a86579 feat: add cron scheduling mode for image update checks (#1460)
* feat: add cron scheduling mode for image update checks

Adds a cron scheduling mode alongside the existing fixed-interval
dropdown in Settings > Automation > Image update checks. Users can
now set a 5-field cron expression (e.g. "0 3 * * 1") for precise
time-of-day scheduling of registry polls.

- Backend: ImageUpdateService gains mode/cronExpression fields and
  cron-based nextDelayMs() using the existing cron-parser dependency.
  PUT /api/image-updates/interval extended with transactional writes
  and server-authoritative cron validation matching the Scheduled
  Operations contract. Nicknames like @daily are supported.
- Frontend: UpdatesSection gains a SegmentedControl toggle and cron
  text input with cronstrue-powered live description. The frontend
  does advisory validation only; backend 400s are surfaced inline.
  SettingsPrimaryButton used for explicit "Save schedule" action.
- No cron jitter (the user chose a specific time). Interval mode
  keeps existing ±10% jitter.
- Tests: 15 new backend tests covering valid cron, invalid cron,
  6-field rejection, nickname support, backward compat, runtime
  fallback, and transactional writes.
- Docs: auto-update-policies.mdx, alerts-notifications.mdx, and
  openapi.yaml updated with new scheduling mode.

* fix: add mode and cronExpression to UpdatesSection test fixtures

The existing tests failed because the mock status object was missing
the new required fields (mode, cronExpression) added with cron
scheduling support. Without them, status.mode was undefined, causing
uiMode to never match 'interval' and the Select combobox to not render.

* fix: prevent SegmentedControl from stretching full-width in SettingsField

The flex-col container defaults items to align-self: stretch, making the
Interval/Cron toggle bar span the full card width. Add self-start so it
sizes to its content.
2026-06-25 19:47:57 -04:00
Anso 058cf8f2c7 feat: make image-update check cadence configurable and visible (#1377)
* feat: make image-update check cadence configurable and visible

The background image-update scanner polled registries on a hardcoded
6-hour interval, with no way to see when it last ran or when the next
run was due. Operators testing updates read this as auto-update being
unreliable: a manual update checks the registry immediately and applies,
so the slow background scan rarely raised the "update available"
notification before the stack was already current.

Backend:
- ImageUpdateService reads image_update_check_interval_minutes (15-1440,
  default 120) and drives a single generation-guarded self-rescheduling
  timer with 10% per-run jitter so fleet nodes do not poll in lockstep.
  restartPolling() applies a new interval live, with no restart, and
  cannot leave a duplicate timer when a save lands mid-scan.
- GET /api/image-updates/status now returns checking, intervalMinutes,
  lastCheckedAt, nextCheckAt, and the manual-cooldown fields. New
  admin-only PUT /api/image-updates/interval persists the setting and
  reschedules.

Frontend:
- New Settings > Automation > Image update checks section to choose the
  interval (read-only for non-admins; admin enforced on the backend).
- The Auto-Update readiness view shows last-checked, next-check, and a
  ticking manual-recheck cooldown, and the copy distinguishes registry
  detection from scheduled auto-update execution.

Adds backend unit and route tests and frontend component tests, and
updates the auto-update documentation.

* fix: drop stale image-update status response in the readiness strip

loadCadence() ran on mount and again after a Recheck with no request
token, so a slow initial /image-updates/status response could resolve
after the recheck-triggered one and overwrite the fresh cooldown with
stale data, or set state after the view unmounted. Guard setCadence with
a monotonic token mirroring loadReadiness, and bump it on unmount. Adds a
regression test for the out-of-order resolution.
2026-06-15 20:06:13 -04:00
Anso 38aabe7064 feat: health-gated updates and rollback readiness (#1354)
* feat: classify stack deploy and update failures with suggested next actions

Failed deploy and update responses now carry a failure classification
(cause category, headline, and suggested next step) derived from the
compose error output. The recovery panel and chip render the
classification and include it in copied diagnostics, and gateway-style
failures surface as a node-unreachable cause.

* feat: add update and rollback readiness reports for stacks

Before a manual update, Sencho now shows an advisory readiness verdict
computed from the stored preflight result, open drift findings, live
container health, the pending image change, the rollback backup slot,
and node disk headroom. The Stack Dossier gains a rollback readiness
section that states what a rollback can restore and explicitly
discloses that volume and bind-mounted data are not covered. Toolbar
and sidebar updates now share one update path, and admins can create a
fleet snapshot from the readiness dialog before updating. Nodes that do
not advertise the capability keep the direct update flow.

* feat: observe stack health after updates with a post-deploy health gate

After a deploy or update succeeds, Sencho now watches the stack for a
configurable observation window and records a passed, failed, or
unknown verdict: containers must stay running, healthchecks must report
healthy, and restart loops or disappearing containers fail the gate.
The deploy panel shows the observation live and holds off auto-closing
until the verdict lands, a failed gate surfaces the existing recovery
actions including rollback, and the stack timeline records update
started and gate verdict events. Scheduled, webhook, bulk, and
git-source updates are gated the same way; rollbacks and installs are
deliberately not. The gate is observational only and can be tuned or
disabled per node under host alert settings.

* docs: document health-gated updates and rollback readiness

New operator page covering the update readiness dialog, the post-update
health gate and its settings, the rollback readiness disclosure, and
classified failures, with cross-links from the atomic deployments and
deploy progress pages. The API reference gains the readiness and
health-gate endpoints, the healthGateId success field, and the failure
classification schema on deploy and update error responses.

* feat: withhold the success verdict while the health gate observes

An update used to show a green Succeeded that a failed health gate then
contradicted moments later. The deploy modal now reports Verifying
health while the gate observes, shows success only when the gate
passes, and makes a failed or unknown gate the headline result; success
toasts soften to a verifying message while a gate runs. The mobile
recovery card groups its actions behind one bottom-right Take action
menu so it stays compact on a phone, with the classified cause still
visible on the card. A successful image update now also counts as the
last known-good marker in rollback readiness, and the docs gain
screenshots of the readiness dialog, gate states, dossier section, and
settings.

* fix: harden log format strings and the env existence path check

Log calls that interpolated the stack name into the console format
string now use constant format strings with placeholder arguments, and
envExists validates path containment inline at its filesystem access,
matching the established patterns used elsewhere in the same files.

* test: adapt deploy modal success specs to the post-deploy health gate

The deploy feedback modal now withholds its success verdict while the
health gate observes the new containers, showing "Verifying health"
until the gate passes. The two success-path E2E tests waited for
"Succeeded" within the gate's 90s default window and timed out.

Shorten the observation window to the 15s minimum for these tests via
the settings API, assert the verify-then-succeed sequence the modal
actually renders, and restore the default window afterward so the test
value does not leak into later runs.

* fix: serialize health gate polling and harden gate observation

Address race conditions in the post-update health gate found in review.

Backend: the gate poller used setInterval, so a Docker observe slower
than the 5s tick could overlap the next poll and corrupt the restart and
missing-container accounting, and a wedged socket could leave a poll
pending forever. Polling is now single-flight: each cycle self-schedules
the next only after it settles, and the observe is bounded by an 8s
timeout so a hung probe counts as a poll error and resolves the gate
unknown after three in a row.

Frontend: the gate poller could overlap requests, letting a slow earlier
"observing" response overwrite an already-applied terminal verdict. It is
now single-flight with a terminal latch, so a late response can never
roll the UI back from passed or failed.

Also reject a non-digit nodeId on the snapshot coverage route instead of
letting parseInt coerce it, document that turning off the deploy progress
panel opts out of the live gate UI while the gate still runs server-side,
and add gate-coverage tests for the webhook, git source, and auto-update
apply paths plus the new single-flight, observe-timeout, and recovery
cases.
2026-06-11 00:26:26 -04:00
Anso 865d792874 feat(pricing): collapse to two tiers (#1309)
* 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.
2026-06-04 17:45:53 -04:00
Anso ca346916c1 fix(auto-update): paid-gate execute route and harden image-check watchdog (#1257)
* fix(auto-update): paid-gate execute route and harden image-check watchdog

Auto-update execution is a paid capability, but POST /api/auto-update/execute
was reachable by any admin regardless of license. Add the paid guard so it
matches the rest of the surface (scheduled-task management and fleet refresh).
The scheduler dispatch to remote nodes still works because the controlling
instance forwards its tier with the request.

Restrict GET /api/image-updates/fleet to admins. The single-node status
endpoint that drives the sidebar update dot stays open to all roles.

Replace the image-check watchdog timer that released the run lock after five
minutes. On a healthy but slow scan it let a manual refresh start a second
concurrent check, duplicating notifications and racing the status writes. The
scan now owns its lock for its full duration, and every Docker socket and
filesystem read is bounded so a wedged daemon or mount cannot stall a scan
forever.

* test(auto-update): assert the debug skip-log branch in the image-check guard

The concurrency-guard test covered the warn branch for a trigger arriving past
the long-run threshold but never exercised the developer-mode debug skip log.
Add a case that enables developer mode and asserts the debug line fires for a
mid-scan trigger under the threshold.
2026-05-31 16:00:00 -04:00
Anso 58df1a50b3 feat(auto-update): show pending image updates fleet-wide on the Auto-Updates page (#770)
Group readiness cards by node so updates pending on every reachable node
are visible without having to switch the active node. Apply now targets
the owning node directly, and Recheck fans out to every reachable node
in parallel; per-node cooldowns are surfaced in the toast.

Adds POST /image-updates/fleet/refresh and invalidates the fleet
aggregation cache after auto-update execute so the next read reflects
the new state immediately. A small banner appears under the hero when
some online nodes did not respond within the request timeout.
2026-04-25 08:21:50 -04:00
Anso f5eb993f48 refactor(backend): add tests then extract metrics and image-updates routers (phase 4b follow-up) (#738)
Wraps up Phase 4 Round B by tackling the two deferred groups. 25 new
integration tests land first and run green against the inline monolith,
then each group is extracted byte-for-byte.

index.ts drops from ~3,678 to ~3,231 lines; test count rises 1,320 → 1,345.

New coverage:
- metrics-routes.test.ts (11) — auth + shape checks for /api/stats,
  /api/metrics/historical, /api/system/stats, /api/system/cache-stats
  (admin-only), and SSE headers for /api/logs/global/stream
- image-updates-routes.test.ts (14) — auth, admin gating, rate-limit
  tolerance on /refresh, fleet aggregation, /auto-update/execute input
  validation and no-stacks short-circuit

New route files:
- routes/metrics.ts — /stats, /metrics/historical, /logs/global (+ SSE
  /stream), /system/stats, /system/cache-stats. Mounted at /api so the
  mixed sub-paths line up.
- routes/imageUpdates.ts — /api/image-updates CRUD + fleet aggregation,
  plus a separate autoUpdateRouter mounted at /api/auto-update that
  owns the /execute handler. Same split pattern as license.ts +
  systemUpdateRouter.

index.ts trims unused imports left behind by the extraction:
globalDockerNetwork, si, STATS_CACHE_TTL_MS, SYSTEM_STATS_CACHE_TTL_MS,
GlobalLogEntry + log-parsing helpers.
2026-04-23 21:49:58 -04:00