Commit Graph

83 Commits

Author SHA1 Message Date
Anso 7d78c9fe22 feat(auth): add TOTP two-factor authentication with backup codes (#615)
* feat(auth): add TOTP two-factor authentication with backup codes

Adds RFC 6238 time-based one-time password support to every tier,
integrated with the existing password and SSO login paths.

Backend:
- New MfaService wrapping otplib with a plus or minus 1 step tolerance,
  base32 secret generation, and hashed single-use backup codes (bcrypt).
- user_mfa and mfa_used_tokens tables in DatabaseService. The second
  table is a DB-backed replay blacklist, purged on a 60s interval.
- authMiddleware now recognizes an mfa_pending scope. A token carrying
  that scope is rejected on every route except the MFA challenge and
  logout, so no API surface is reachable before the second factor
  clears.
- /api/auth/login issues only a short-lived mfa_pending cookie when the
  user has MFA enrolled. /api/auth/login/mfa consumes that cookie,
  verifies the code (or backup code), and swaps in a real session.
- /api/auth/mfa/* routes for status, enrol/start, enrol/confirm,
  disable, backup-code regenerate, and SSO-bypass opt-in.
- Admin recovery path: POST /api/users/:id/mfa/reset clears the target's
  MFA state, bumps token_version, and writes an audit log entry.
- CLI emergency fallback: backend/src/cli/resetMfa.ts is wired via
  `npm run reset-mfa <username>` and also exported for tests.
- SSO flows (LDAP and OIDC) gate on user_mfa.sso_enforce_mfa before
  issuing a session; default behaviour keeps the SSO path frictionless.
- Per-user lockout after 5 consecutive failed codes (15 min).

Frontend:
- AppStatus gains an mfa-challenge branch driven by /api/auth/status.
- New MfaChallenge screen, MfaEnrollDialog (QR plus manual secret plus
  backup codes), MfaDisableDialog, MfaBackupCodesDialog.
- Account section shows a Two-factor authentication card with enrol,
  regenerate, disable, and the SSO-enforce toggle (shown only when SSO
  providers are configured).
- Users section gains a Reset 2FA action for admins.

Docs:
- New user guide at features/two-factor-authentication.mdx.
- New admin guide at operations/two-factor-admin.mdx.
- SSO page cross-links to the 2FA doc.

* fix(mfa): drop unused TEST_PASSWORD import and stale eslint disable

* fix(mfa): simplify e2e openAccountSettings helper to match working pattern

* fix(mfa): make e2e suite self-contained and always clean up

Test #2 called loginAs() before the MFA challenge step, which waited for
the dashboard indicator that never appears once the previous test enrolled
the user. That timeout skipped the rest of the serial block, including
the disable step, leaving MFA enabled and breaking every later spec.

Two fixes:

- Tests #2 and #3 now navigate directly to the login page instead of
  piggybacking on loginAs, which only handles the password-only path.
- A new afterAll hook unconditionally disables MFA via the API using two
  unused backup codes, so the DB is reset even if a test fails midway.

* fix(e2e): use backup code for mfa recovery to avoid totp replay race

The final recovery step in the backup-code replay test previously
generated a fresh TOTP to sign back in. When the timing landed inside
the same 30-second window that test #2 consumed, the server's replay
blacklist correctly rejected it, producing a ~50% flake rate. Backup
codes are single-use and sidestep the replay window, so the recovery
becomes deterministic.

* fix(e2e): drive mfa disable test through the challenge screen

Test #4 called loginAs after test #3 left MFA enabled, but loginAs
waits for the dashboard indicator and does not handle the challenge
screen, so it timed out. Drive the login manually, satisfy the
challenge with a backup code, and use a backup code for the disable
step too to avoid any TOTP replay-window race against earlier tests
in the serial block.
2026-04-15 18:45:51 -04:00
Anso 6529a24530 feat(git-sources): harden create-from-git with LFS + submodule warnings (#609)
* feat(git-sources): surface LFS and submodule warnings on create

Creating a stack from a Git repo now detects two common anomalies and
tells the user about them rather than silently producing broken stacks.

- LFS-pointer compose/env files fail early with a clear error instead
  of writing a 130-byte pointer stub to disk as real content.
- Repositories containing .gitmodules produce a non-fatal warning so
  the user knows build contexts or volumes inside submodules will be
  empty at deploy time.

Also refines the create dialog: sr-only DialogDescription for a11y,
short commit SHA suffix on the success toast, env-path hint under the
"Sync .env" checkbox showing which path will be read, and a route-level
diagnostic log line gated on developer mode for support debugging.

* test(git-sources): cover LFS, submodule, and nested env_path paths

Adds unit coverage for the new LFS-pointer rejection and submodule
warning plumbing, plus a nested compose_path case that exercises the
default env_path resolution ("apps/web/compose.yaml" with sync_env on
and env_path unset writes "apps/web/.env" both to disk and to the DB).

Extends the E2E suite with a happy-path assertion that the full-length
commit SHA is returned in the create response, and a UI flow that
verifies the short-SHA suffix appears in the success toast.

* docs(git-sources): add troubleshooting for LFS, submodules, HTTPS-only

Adds troubleshooting entries for the newly surfaced LFS and submodule
anomalies, expands the clone-timeout entry with the bounded-fetch
explanation, and adds a dedicated HTTPS-only entry. Also consolidates
the known limitations into a single list covering LFS, submodules,
branch-tracking, and HTTPS-only.

* fix(settings): use Route icon for notification routing

The routing section in Settings previously used GitBranch, which now
clashes with the Git Source feature's icon across the editor. Switch
to Route (a branching-flow glyph) so routing rules have a distinct
visual identity and aren't visually conflated with Git-backed stacks.

* fix(git-sources): return 400 for upstream auth failures and disambiguate 404s

Upstream git-host auth failures were mapping to HTTP 401, which the frontend
apiFetch treats as a Sencho session expiry and fires the global logout event.
They now return 400 with code=AUTH_FAILED in the body so the UI can branch on
the discriminator without logging the user out. The status mapping moved into
utils/gitSourceHttp so it can be unit-tested without booting the app.

mapGitError also relied on the HttpError class alone, so any non-2xx response
(including 404) was classified as auth failure. It now inspects the numeric
status on err.data and considers whether a token was supplied, producing more
actionable messages for missing repos, private repos, and wrong-scope tokens.
2026-04-15 11:31:29 -04:00
Anso 3955267bbe feat(git-sources): create a stack from a Git repository (#606)
* refactor(git-sources): extract GitSourceFields from GitSourcePanel

Pure extraction of the repo/branch/path/auth/apply-mode form fields into a
reusable controlled component so the upcoming Create Stack from Git flow can
render the same form in the Create Stack dialog. No behavior change.

* feat(git-sources): create a stack from a Git repository

Add a From Git tab to the Create Stack dialog so users can name a new
stack, point it at a repo + branch + compose path, and have the compose
fetched, validated, written to disk, and linked in one shot. Optional
deploy-after-create runs the initial bring-up when requested.

Backend: new POST /api/stacks/from-git route gated by stack:create.
GitSourceService.createStackFromGit() fetches and validates before
touching disk, then creates the stack, writes the compose (and .env if
sync is enabled), and seeds the git source row with the fetched commit
so future pulls produce a clean diff. Runs under the per-stack lock so
a concurrent webhook cannot race the create. Deploy failure is
non-fatal and surfaced to the caller.

Frontend: the existing Create Stack dialog is now tabbed, with Empty
keeping the original single-field flow unchanged.

* test(git-sources): cover create-from-git endpoint and e2e flow

Service tests verify createStackFromGit seeds the last_applied columns
on success, writes the env file when sync is enabled, refuses an
invalid apply-matrix without fetching, rejects invalid compose
without leaving orphan state, and rolls back the on-disk stack dir
when a post-create step fails.

Route tests cover auth, missing stack_name, invalid stack name,
http:// rejection, oversized repo_url, and the 409 collision guard.

E2E adds a Create-stack-from-Git block covering tab visibility,
client-side HTTPS check, backend .git/config rejection, and a
happy-path fetch against a public demo repo (skipped on network
failure).

* docs(git-sources): document create-stack-from-git tab

Add a new section near the top describing the From Git tab in the
Create Stack dialog: what it does, the Deploy after create checkbox,
and the four failure modes (name collision, unreachable repo,
invalid compose, deploy-after-create failure).
2026-04-15 08:05:10 -04:00
Anso 00901cf5bf fix(git-sources): harden validation, RBAC, concurrency, and deploy recovery (#603)
* fix(git-sources): harden validation, RBAC, concurrency, and deploy recovery

Tightens the surface area around the Git source feature:

- Enforce HTTPS-only repo URLs server-side (regex was permissive).
- Add stack:read permission check on git-source reads and filter the
  list endpoint by callable permission.
- Validate stack names before permission checks on mutation routes so
  scoped lookups never see unvalidated input.
- Cap repo_url / branch / compose_path / env_path / token lengths and
  require the stack directory to exist before upsert.
- Wrap pull() in the per-stack mutex to eliminate the pull/delete race
  that could orphan pending data.
- Block .git/ path components in compose_path / env_path so a
  misconfigured clone cannot leak repo metadata.
- Return {applied, deployed, deployError?} on deploy failure instead of
  throwing, and surface deployError as a warning toast so the user can
  retry deploy without re-pulling.
- Always clean the stack_git_sources row on stack delete even when the
  file deletion step fails.
- Add shadow-card-bevel to the pending alert and metadata card per the
  design system.
- Handle the new 403 response on the panel fetch gracefully.
- Add diagnostic logging gated on developer_mode (isDebugEnabled) across
  fetch / pull / apply / webhook paths with credential scrubbing.

* test(git-sources): expand coverage for hardening and route validation

- New route-level suite covers HTTPS enforcement, required fields,
  max-length caps on repo_url / branch / compose_path / env_path /
  token, the stack-existence 404 guard, and GET authz.
- Service tests cover the .git metadata guard on compose and env
  paths (including nested and substring-containing "git"), pull and
  apply rejections when no source is configured or pending is
  cleared, the sha-mismatch branch, and the deploy-failure return
  shape that now carries deployError.
- E2E adds three server-side contract assertions: PUT against a
  missing stack returns 404, http:// is rejected with 400, and
  .git/config is rejected as compose_path.

* docs(git-sources): document deploy-failure recovery path

Adds a Troubleshooting entry explaining that when apply succeeds but
the subsequent deploy fails, the compose content is already on disk
and the user can retry deploy from the stack editor without
re-pulling.

* docs(git-sources): add configuration, diff, pending, and webhook screenshots
2026-04-14 22:32:42 -04:00
Anso 377df7e546 feat(git-sources): link stacks to Git repositories with diff-and-apply workflow (#600)
* feat(git-sources): link stacks to Git repositories with diff-and-apply workflow

Add Git Sources so any stack can point at an HTTPS Git repository, branch, and
compose file path. Pulls fetch + validate the incoming commit, store a
diffable pending snapshot, and apply writes only after explicit confirmation
(or automatically, per the configured apply mode). Sibling .env sync is
optional. Works on the Community tier.

Apply modes:
- Review only: mark pending, wait for manual apply in the diff dialog
- Auto-write: write compose + env to disk, do not redeploy
- Auto-deploy: write files and run docker compose up -d

Webhook integration: webhooks can target the new "git-pull" action to trigger
a sync from CI. Per-source debounce prevents runaway pipelines from hammering
the repository host. Tokens are encrypted at rest and never returned to the
frontend.

Docs and tests included. Screenshots and Playwright E2E flows to follow.

* fix(git-sources): drop unnecessary useMemo on commit sha slice

React Compiler's lint rule rejected the manual dependency list because the
inferred dep ('pull') was less specific than the written one ('pull?.commitSha').
The computation is a cheap 7-char slice, so drop the useMemo entirely rather
than fight the rule.

* test(git-sources): add Playwright E2E flows and drop orphan source rows on stack delete

- E2E coverage: non-HTTPS URL rejected client-side, unreachable repo surfaces
  a toast error on save, and configure+remove walks the AlertDialog confirm path.
- Deleting a stack now also drops its linked Git source row so a future stack
  with the same name starts clean rather than inheriting a stale config.
2026-04-14 21:13:17 -04:00
Anso 6275adc6b3 feat(registries): harden Private Registry Credentials feature (#597)
* feat(registries): add stateless test endpoint, ECR caching, URL and host hardening

Adds a POST /api/registries/test endpoint so credentials can be verified
before being persisted. Caches ECR authorization tokens in memory until
their AWS-reported expiry (minus a safety margin) instead of fetching on
every compose invocation. Normalizes registry URLs on save so the
stored values match the keys Docker expects in ~/.docker/config.json,
fixes a bidirectional host-match bug in getAuthForRegistry that could
cross-match overlapping hostnames, and surfaces per-registry decryption
failures as warnings in the deploy log stream instead of swallowing
them. Also strips the Authorization header on cross-host redirects in
the test probe, rejects non-http(s) schemes on save, and validates the
shape of returned ECR authorization tokens before use.

* refactor(registries): align UI with design system and add in-form test button

Swaps the registry type dropdown from shadcn Select to the project's
Combobox, applies the canonical card bevel and top-border hover styling
to the form container and each registry row, restyles the delete
button to the ghost + muted destructive pattern, uses strokeWidth 1.5
on every Lucide icon, and routes all toast errors through the
standard defensive chain. Adds a Test connection button inside the
form so credentials can be verified before saving.

* test(registries): cover RegistryService and deploy warnings surface

Adds unit coverage for URL normalization, the encrypt/decrypt round
trip through create and resolveDockerConfig, exact-host matching in
getAuthForRegistry, resolveDockerConfig warnings on decryption
failure, ECR token cache hit/miss and invalidation on update, the
stateless testWithCredentials path for 200, 401 with and without a
challenge, network errors, and ECR success and failure including
malformed tokens. Extends the ComposeService tests to verify that
warnings from resolveDockerConfig reach the deploy log stream.

* docs(registries): document test-before-save flow and troubleshooting

Describes the in-form Test connection button, the two-point testing
flow from the registries list, the cached ECR token behavior during
deploys, the per-registry warning Sencho emits when a stored secret
cannot be decrypted, and adds a Troubleshooting section covering
common 401 causes, ECR token handling, warning interpretation, and
per-node credential scoping.
2026-04-14 17:58:57 -04:00
Anso 6b8c369745 fix(notifications): stop Sencho version notifications from silently skipping (#594)
* fix(notifications): stop Sencho version notifications from silently skipping

Three independent defects combined to make version-update notifications
silently fail even while Fleet overview correctly surfaced an update button:

- The in-memory 6-hour cooldown was advanced before the network fetch,
  so a single transient failure at boot could lock the check for the
  rest of the container lifetime. Moved the cooldown update inside the
  success branch so failures retry on the next eval cycle.
- MonitorService called the raw version fetch directly, bypassing the
  CacheService wrapper (TTL, inflight dedup, stale-on-error) that Fleet
  uses, so the two paths could diverge. Unified both on a shared
  getLatestVersion() helper in utils/version-check.ts.
- The dedup key could carry stale state from a previous build and never
  self-clear. It now self-heals when the running version reaches the
  previously-notified version, so future releases re-fire as expected.

Added diagnostic logs gated on debug mode for each skip branch, plus
three regression tests covering cooldown-on-failure, cooldown-on-success,
and dedup self-heal.

* docs(notifications): drop legacy-upgrade framing from alerts troubleshooting

Sencho has not shipped publicly, so troubleshooting entries written in
'this used to happen but now does Y' mode reference a past that does not
exist for any reader. Rewrote the version-notification, image-update,
and crash-alert troubleshooting entries to describe current behavior
positively without referring to prior builds, upgrade paths, or legacy
fixes.

* chore(security): accept CVE-2026-33810 in bundled Docker CLI 29.4.0

Trivy now flags CVE-2026-33810 (Go stdlib crypto/x509 DNS constraint
bypass, fixed in Go 1.26.2) in the Docker CLI static binary we ship.
Docker CLI 29.4.0 is the latest upstream release and still links Go
1.26.1; no newer static binary exists yet.

Same exposure profile as the already-accepted CVE-2026-32280: the
Docker CLI and compose plugin only validate certificates from
well-known registry CAs and the local Docker socket, not from
attacker-controlled CAs with crafted DNS name constraints. Revisit
on the next Docker CLI release that rebuilds against Go 1.26.2 or
later.
2026-04-14 16:34:28 -04:00
Anso 44a89d9d2e fix(docker-events): harden crash detection against edge cases (#591)
* fix(docker-events): harden crash detection against edge cases

- Isolate per-container failures in the reconcile path so one failed
  container inspect cannot abort classification for the rest of the
  batch after a reconnect.
- Fall back to inspecting State.OOMKilled when a container exits with
  code 137 and no oom event preceded the die, so cgroup OOM kills that
  lose the oom event are still classified correctly. The dedup check
  runs before the fallback so crashloops do not hammer the daemon.
- Guard the intentional-kill window against wildly future-dated Docker
  timestamps by switching to a signed age comparison with a bounded
  negative-skew tolerance, so clock skew cannot flip a genuine crash
  into an intentional stop.
- Align diagnostic logging with the codebase [Service:diag] convention
  and add one informational lifecycle log on boot and shutdown so
  operators running in developer mode can confirm the watcher started.
- New tests cover gap-inspect isolation, the OOM inspect fallback (both
  success and failure paths), duplicate die events collapsing within
  the grace window, and clock-skew bounds on the classifier.

* docs(alerts): add troubleshooting entries for crash detection toggle and rate limits
2026-04-14 14:37:28 -04:00
Anso ad9a6859e6 fix(notifications): replace polling with Docker event stream for container lifecycle detection (#588)
* fix(notifications): replace polling with Docker event stream for container lifecycle detection

Replaces the 30-second MonitorService crash-detection poll with a causal,
per-node Docker events stream. Eliminates false crash alerts on intentional
stops (docker stop, compose down, stack restart/update), detects OOM kills
as a distinct alert category, and surfaces real crashes in real time.

A new DockerEventManager spawns one DockerEventService per local node. Each
service consumes the filtered container event stream, classifies die events
against recent kill/oom state, and reconciles container state via snapshot
diffing on connect and reconnect. Rate limiting, exponential backoff with
jitter, and parse-error tolerance keep the stream resilient under load and
during daemon interruptions.

MonitorService retains host limits, janitor, version check, and stack metric
alerts; crash and healthcheck detection move out entirely.

* fix(tests): silence require-imports lint in hoisted mock factory
2026-04-14 13:47:48 -04:00
Anso 4a0319331a fix(notifications): resolve version notification showing 0.0.0 and backfill missing image update notifications (#586)
- Read running Sencho version from the packaged manifest via getSenchoVersion() instead of process.env.npm_package_version, which is undefined when launched via node dist/index.js (production Docker). Skip the update check entirely when the version cannot be resolved.
- Add a one-time backfill pass in ImageUpdateService so users who upgraded to a Sencho version with the notification pipeline receive a catch-up entry for stacks already flagged as having updates before the upgrade.
- Surface dispatch failures as error-level entries in the in-app notification bell via a direct notification_history write, so misconfigured webhooks are visible without tailing logs.
- Extend test coverage for both paths: mock getSenchoVersion (including the null-version case) and add dispatch-path tests for transition, no-re-fire, backfill, and error surfacing.
- Expand alerts-notifications docs with per-instance 6-hour check cadence, an example version message, a backfill note, and three new troubleshooting entries.
2026-04-14 11:51:30 -04:00
Anso 23fc702296 fix(network-topology): harden with edge-case fixes, logging, and test coverage (#583)
* fix(network-topology): harden with edge-case fixes, logging, and test coverage

Fixes stale data on node switch (topology now refreshes when active node changes),
adds manual refresh button to toolbar, and guards against containers with empty
Names arrays by falling back to a 12-char short ID. Adds operational and diagnostic
logging around topology fetches, and introduces 25 unit tests covering happy paths,
system network filtering, stack resolution, container deduplication, edge cases,
and error handling.

* docs: refresh screenshots
2026-04-14 10:59:11 -04:00
Anso 718c1eb1ea fix(host-console): harden with security fixes, validation, and test coverage (#580)
Security:
- Enforce RBAC (system:console permission) on WebSocket upgrade
- Validate token_version for user sessions on WS connections
- Expand env var sanitization to cover additional secret patterns
  (PRIVATE, AUTH, PASSPHRASE, ENCRYPT, SIGNING) and connection
  strings (REDIS_URL, MONGO_URI, AMQP_URL, DSN)

Reliability:
- Add session tracking with max 5 concurrent console sessions
- Add WebSocket heartbeat (30s ping, 60s pong timeout) to detect
  and clean up dead connections and orphaned PTY processes
- Differentiate PTY spawn error messages (shell not found,
  permission denied, generic failure)
- Guard against duplicate cleanup when both WS close and PTY exit
  fire

Observability:
- Add structured logging with [HostConsole] prefix for session
  lifecycle (open, close, duration, user, pid)
- Add diagnostic logging behind developer_mode for terminal resize
  events and message parse errors

Frontend:
- Replace hardcoded hex colors with oklch CSS custom properties
  (--terminal-bg, --terminal-fg, --terminal-cursor, etc.)
- Apply design system material tokens (shadow-card-bevel, recessed
  well shadow, card border hierarchy, strokeWidth 1.5)

Tests:
- Add 20 tests covering env sanitization patterns (10 keyword
  categories + safe vars + case insensitivity), session tracking,
  and console-token RBAC (admin, viewer, deployer, API tokens)

Docs:
- Document admin role requirement and session limits
- Add troubleshooting section (session limits, shell not found,
  proxy timeouts, missing console tab)
- Update security section with expanded env var coverage
2026-04-14 10:06:59 -04:00
Anso c4ff58347e fix(container-exec): harden with security fixes, validation, and test coverage (#577)
* fix(container-exec): harden with security fixes, validation, and test coverage

- Enforce admin role at WebSocket upgrade for container exec sessions
- Validate container is running before creating exec
- Fix bash-to-sh fallback (move .start() inside try/catch)
- Register container-exec as a capability for fleet visibility
- Add standard and diagnostic logging for exec lifecycle
- Fix false Admiral license claim in API docs
- Fix design system violations in BashExecModal (hardcoded colors)
- Remove duplicate legacy xterm dependencies
- Add 18-test suite covering auth, validation, fallback, and cleanup

* fix(container-exec): use correct Duplex type for exec stream

The stream variable was typed as NodeJS.ReadWriteStream, which lacks
.destroy(). Dockerode's Exec.start() returns stream.Duplex per its
type definitions. This caused tsc to fail while Vitest (which skips
full type checking) passed.
2026-04-14 08:23:05 -04:00
Anso 0a94df318a fix(alerts): harden with security fixes, design compliance, and test coverage (#570)
* fix(alerts): harden with security fixes, design compliance, and test coverage

Add authMiddleware to all alert endpoints, validate notification test
dispatch inputs, fix restart_count metric via Docker inspect, correct
network metric units, replace any types with DockerContainerStats
interface, add webhook timeouts and dispatch error tracking.

Frontend: migrate Select to Combobox, add ScrollArea and delete
confirmation AlertDialog, fix icon strokeWidth to 1.5.

Add update availability notifications for both Sencho version updates
(6-hour check in MonitorService) and stack image updates (state
transition detection in ImageUpdateService). Extract shared version
fetch logic into utils/version-check.ts.

Add diagnostic logging gated behind developer_mode for MonitorService
breach state machine and NotificationService dispatch routing.

Tests: 24 new alert API integration tests, restart_count and version
check unit tests (688 total passing). Docs updated with HTTPS
requirement, update notifications section, and troubleshooting guide.

* fix(alerts): remove unused TEST_USERNAME import in alerts-api tests
2026-04-13 21:40:53 -04:00
Anso e0d1ca9dc0 fix(api-tokens): harden with security fixes, design compliance, and test coverage (#567)
Security: add JWT-level expiry ceiling (400d), per-user token count limit (25),
and token name uniqueness enforcement. Fix async clipboard copy.

Design: migrate Select to Combobox, apply card bevel styling, fix icon
strokeWidth, add tabular-nums to timestamps, fix destructive button pattern.

Tests: expand from ~20 to 47 test cases covering creation validation, token
limits, name uniqueness, last_used_at tracking, ownership constraints, delete
edge cases, and registry blocked endpoints.

Docs: update API Tokens docs with token limits, name uniqueness, registry
restrictions, and JWT expiry ceiling. Update OpenAPI spec with 409 response.
2026-04-13 18:32:07 -04:00
Anso 1d89e8ce59 fix(sso): harden SSO with role sync, security fixes, design compliance, and test coverage (#564)
Rate-limit OIDC callback route and clear state cookie on all paths.
Validate GitHub API responses and add server-side config validation.
Sync SSO user roles on every login respecting seat limits.
Add LDAP connection timeout, bind DN warning, and multiple entry logging.
Replace Select with Combobox and apply card design tokens in SSOSection.
Expose OIDC scopes field in settings UI.
Fix hardcoded colors to use design system tokens.
Add standard and diagnostic logging throughout SSO flow.
Add tests for role sync, seat limits, LDAP escaping, and config validation.
Remove unused login-form.tsx template.
Update docs and screenshots for SSO feature.
2026-04-13 17:46:38 -04:00
Anso 6daa7a135d fix(audit): harden audit log with summary fixes, design compliance, and test coverage (#561)
* fix(audit): harden audit log with summary fixes, design compliance, and test coverage

Replace stale /compose/* route summaries with current /stacks/* patterns,
add ~30 missing route summaries for container ops, labels, fleet updates,
SSO, templates, and auto-update. Fix X-Forwarded-For extracting full
proxy chain instead of first client IP. Pre-sort pattern table at module
load for O(1) lookup instead of sorting per request.

Add Calendar and DatePicker UI components (react-day-picker v9 + date-fns)
replacing browser-native date inputs. Align AuditLogView with design system:
Combobox instead of Select, design token colors, strokeWidth 1.5, card bevel,
tabular-nums on all numeric cells.

Add diagnostic logging gated behind developer mode for audit middleware,
query, and export endpoints. Extract getAuditSummary to testable utility.
Add 32 tests covering summary resolution, DB round-trips, API permissions,
export formats, and middleware integration.

* fix(audit): use correct Chevron prop types for react-day-picker compatibility
2026-04-13 15:38:33 -04:00
Anso c261fbc327 fix(rbac): harden user management with token versioning, session invalidation, and test coverage (#558)
Security fixes:
- Map deploy-only API tokens to deployer role (not admin)
- Add token versioning to invalidate sessions on password/role changes
- Reject deleted users immediately in auth middleware (no 24h JWT grace)
- Use DB role instead of JWT role so changes take effect instantly
- Block password setting on SSO-provisioned users
- Check proxy variant in scoped permission resolver

Cleanup and logging:
- Remove orphaned role assignments when stacks/nodes are deleted
- Add standard logging for login, user CRUD, role assignments, password changes
- Add diagnostic logging gated behind developer_mode setting
- Extract issueSessionCookie helper (DRY across 5 JWT signing sites)

Frontend:
- Replace Select with Combobox in UsersSection (design system compliance)
- Add strokeWidth={1.5} to Lucide icons
- Hide password fields for SSO-provisioned users

Testing:
- Add 42-test RBAC suite covering user CRUD, token versioning, scoped
  assignments, permissions endpoint, password management, seat limits,
  last-admin protection, and orphan cleanup
2026-04-13 14:31:34 -04:00
Anso 809bf76c20 fix(fleet): harden fleet snapshots with DRY capture, audit fixes, and design compliance (#555)
Extract duplicated snapshot capture functions from index.ts and
SchedulerService.ts into a shared module (snapshot-capture.ts). Fix
audit log route patterns that used singular 'snapshot' instead of
plural 'snapshots'. Apply design system to FleetSnapshots component
(card styling, strokeWidth, font-mono, tabular-nums, ScrollArea).
Add safePage pagination, loading toast for creation, and fix the
restore dialog race condition with a controlled AlertDialog. Add
diagnostic logging gated behind Developer Mode. Add tests for
restore endpoint, admin role enforcement, and edge cases. Update
docs with troubleshooting section and refresh screenshots.
2026-04-13 13:26:55 -04:00
Anso a695251f38 fix(labels): harden stack labels with nodeId filtering, concurrency guard, and test coverage (#552)
* fix(labels): add nodeId filter, existence check, stale cleanup, concurrency guard, and body validation

- Fix getStacksForLabel to filter by node_id (prevents cross-node data leak)
- Add getLabel(id, nodeId) for single-label existence check
- Add getLabelCount(nodeId) for enforcing per-node label limit (50)
- Add cleanupStaleAssignments to remove orphaned assignments for deleted stacks
- Add label/assignment cleanup to deleteNode transaction
- Add label existence check on bulk action endpoint (returns 404 for missing labels)
- Add concurrency guard on bulk actions (returns 429 if already in-flight)
- Add requireBody guard on all mutation endpoints
- Extract isSqliteUniqueViolation helper to deduplicate constraint checks
- Add MAX_LABELS_PER_NODE constant (50) with limit enforcement on create
- Add diagnostic logging on all label endpoints (gated behind developer_mode)
- Add operational log line for bulk action results

* fix(labels): use ScrollArea, show failure details, add loading feedback, deduplicate constants

- Replace overflow-y-auto div with ScrollArea in LabelAssignPopover (design system)
- Show failed stack names in bulk action error toast
- Add loading toast for context menu label toggle
- Disable bulk action menu items while a bulk action is running
- Disable "New Label" button at 50-label limit with "Limit reached" text
- Export LABEL_COLORS and MAX_LABELS_PER_NODE from LabelPill (single source of truth)
- Import shared constants in LabelAssignPopover and LabelsSection (remove duplicates)
- Add BulkActionResult interface to replace inline type assertion

* test(labels): add comprehensive coverage for label CRUD, assignments, and bulk edge cases

42 tests covering:
- getLabels: empty, ordered, node isolation
- getLabel: found, wrong node, nonexistent
- createLabel: returns with ID, duplicate name constraint
- getLabelCount: correct count, zero for empty node
- updateLabel: name, color, both, not found, wrong node
- deleteLabel: removes label, cascades assignments, wrong node no-op
- setStackLabels: assign, replace, clear, invalid ID throws
- getLabelsForStacks: correct mapping, empty result
- getStacksForLabel: correct results, node filter, empty for nonexistent
- cleanupStaleAssignments: removes stale, preserves valid, handles empty
- deleteNode: cascades labels and assignments
- Edge cases: atomicity, cascade across stacks, multi-label assignment

* docs(labels): document 50-label limit and bulk action failure details

* fix(labels): add missing LabelColor type imports and explicit parameter types

* refactor(labels): extract label types and constants to label-types.ts

Moves LabelColor, Label, LABEL_COLORS, and MAX_LABELS_PER_NODE out of
LabelPill.tsx into a dedicated non-component file. This fixes the
react-refresh/only-export-components lint error caused by mixing
constant exports with component exports.
2026-04-13 12:49:03 -04:00
Anso 44e8fdfba9 fix(scheduler): harden scheduled operations with stale cleanup, cron validation, and design fixes (#549)
* fix(scheduler): clean up stale runs on startup and auto-disable invalid cron tasks

- Add markStaleRunsAsFailed() bulk DB method with status index
- Clean up orphaned 'running' records on scheduler startup
- Auto-disable tasks when cron expression becomes invalid at execution time
- Promote CRUD debug logs to standard logs for scheduled task admin actions
- Add diagnostic logging for task pre-checks, action timing, and prune fallback

* refactor(scheduling): extract shared types and fix design system violations

- Extract ScheduledTask, TaskRun, NodeOption to shared types file
- Extract getCronDescription and formatTimestamp to shared utilities
- Fix formatTimestamp falsy-zero null check
- Tighten last_status type to 'success' | 'failure' | null
- Add strokeWidth={1.5} to all action icons per design system
- Add sr-only DialogDescription for accessibility
- Wrap Sheet run history in ScrollArea
- Fix delete button styling to match design system pattern
- Change manual trigger toast from "executed" to "triggered"

* test(scheduler): add tests for snapshot, remote update, stale cleanup, and cron invalidation

- Add stale run cleanup tests (bulk markStaleRunsAsFailed, logging)
- Add cron invalidation test (auto-disable, error message)
- Add executeSnapshot tests (fleet capture, empty stacks)
- Add executeUpdateRemote tests (proxy success, remote error)
- Document stale run cleanup and cron auto-disable in troubleshooting docs
2026-04-13 11:32:09 -04:00
Anso a17b16b258 fix(scheduler): harden auto-update policies with cascade deletes, error reporting, and UI fixes (#545)
* fix(scheduler): harden auto-update policies with cascade deletes, error reporting, and UI fixes

- Fix orphaned task runs on policy/node deletion with transaction-wrapped cascade deletes
- Make manual trigger non-blocking (202 Accepted) to prevent proxy timeouts
- Distinguish registry check failures from clean "no update" results via structured ImageCheckResult
- Trim whitespace-only policy names in both frontend and backend validation
- Add strokeWidth={1.5} to action icons per design system
- Add sr-only DialogDescription for Radix accessibility
- Replace Select with Combobox for frequency picker
- Wrap run history sheet content in ScrollArea
- Support concurrent Run Now indicators via Set-based state
- Abort stale stack fetches on node switch with AbortController
- Add standard and diagnostic logging to SchedulerService and ImageUpdateService
- Add tests for cascade deletes, image checking, and scheduler edge cases
- Add troubleshooting section to auto-update docs

* fix(tests): resolve lint errors in image-update-service tests

Remove unused mock variables (mockGetImage, mockGetDocker) and unused
ImageCheckResult type import. Replace CommonJS require('yaml') with
ESM import to satisfy no-require-imports rule.

* chore(deps): bump Docker CLI to 29.4.0 and Compose to v5.1.2

Resolves Trivy CVE-2026-32282 (Go stdlib symlink follow in Root.Chmod)
by upgrading to releases that ship Go 1.25.9. Compose v5.1.2 also bumps
grpc to 1.80.0, resolving CVE-2026-33186.

* chore(security): accept CVE-2026-32282 in .trivyignore, update stale refs

Go stdlib symlink-following in Root.Chmod (CVE-2026-32282) affects both
Docker CLI 29.4.0 (Go 1.26.1) and Compose v5.1.2 (Go 1.25.8). Fix
requires Go 1.25.9 or 1.26.2; no upstream static binary ships a patched
runtime yet. The vulnerable code path requires a chroot context with
attacker-controlled filesystem, which does not apply to our usage.

Also updates version references from v5.1.1/v29.3.1 to v5.1.2/v29.4.0
for existing CVE entries, and notes that Compose v5.1.2 resolved
CVE-2026-33186 (grpc bumped to 1.80.0) for the compose binary.
2026-04-13 09:49:11 -04:00
Anso d23c6779af fix(fleet): harden remote node updates with admin enforcement, expiry fix, and diagnostics (#542)
- Fix completed-entry auto-expiry using resolvedAt instead of startedAt
- Add admin role enforcement to update trigger and update-all endpoints
- Add missing error field in rejected-promise fallback for update-status
- Fix rejected promises in update-all losing node names
- Harden frontend recheck button with try/catch/finally error handling
- Align frontend isValidVersion with stricter regex validation
- Add diagnostic logging gated behind developer_mode
- Extract resolveTracker helper to centralize terminal state transitions
- Add 15 new fleet test cases covering auth, tier gating, input validation, and admin roles
- Document admin requirement and troubleshooting in fleet-view docs
2026-04-12 22:07:41 -04:00
Anso a74a516850 fix(logs): harden global logs with shared parsing, SSE fixes, and level filter (#539)
- Extract log parsing utilities (normalizeContainerName, parseLogTimestamp,
  detectLogLevel, stripControlChars, demuxDockerLog) to shared module,
  eliminating duplication between polling and SSE endpoints
- Fix timestamp regex to accept timezone offsets (+HH:MM/-HH:MM), not just Z
- Replace any[] with typed GlobalLogEntry interface
- Add SSE heartbeat (30s) to prevent reverse proxy timeouts
- Add X-Accel-Buffering: no header for nginx SSE compatibility
- Replace swallowed catch blocks with console.warn diagnostics
- Fix SSE not reconnecting on node switch (missing dep in effect array)
- Add settings change event so devMode/pollRate updates apply without remount
- Add log level filter (ALL/ERROR/WARN/INFO) to toolbar
- Replace overflow-auto div with ScrollArea for design system compliance
- Add strokeWidth={1.5} to all toolbar icons
- Include stack name in download format
- Surface fetch errors with inline banner
- Add 39 unit tests for all log parsing utilities
- Update docs with level filter documentation and refreshed screenshot
2026-04-12 21:19:08 -04:00
Anso 1702dabb7a fix(fleet): add auth middleware, input validation, and design system compliance (#536)
Add authMiddleware to all 13 fleet endpoints that were previously
accessible without authentication. Add NaN validation for parseInt
params, stackName validation on snapshot restore, and description
length cap on snapshot creation. Clean up updateTracker entries on
node deletion to prevent memory leaks.

Replace hardcoded colors with design system tokens, swap Select for
Combobox, replace overflow-y-auto with ScrollArea, fix card styling
(shadow-card-bevel, border tokens). Fix stale container data by
always refetching on stack expand with a loading guard against
concurrent requests.

Add operational logging for state-changing fleet operations and
diagnostic logging gated behind Developer Mode. Add 20 fleet tests
covering auth enforcement, input validation, tier gating, and
snapshot CRUD lifecycle.
2026-04-12 20:28:18 -04:00
Anso cd3d7b23be feat(app-store): add port conflict indicator to deploy sheet (#533)
Show a pulsating warning dot next to host ports that are already in
use by a running container. Hovering over the dot reveals which
Sencho-managed stack or external app occupies the port.

Adds GET /api/ports/in-use endpoint that returns a map of bound host
ports with ownership info, and a getPortsInUse method on
DockerController that reuses the existing container-to-stack
resolution logic.
2026-04-12 19:57:38 -04:00
Anso 5f91e16417 fix(app-store): handle orphaned stack directories on template deploy (#530)
* fix(app-store): handle orphaned stack directories on template deploy

When a stack deployed via the App Store is later removed through Docker
Desktop or the CLI (instead of through Sencho), its directory remains on
disk without a compose file. The deploy endpoint previously rejected any
re-deploy with a 409 if the directory existed, even if empty.

Now the endpoint checks for a compose file before rejecting. If the
directory exists but contains no compose file, it is treated as an
orphaned remnant: cleaned up automatically and the deploy proceeds.

Also makes FileSystemService.hasComposeFile public so the deploy
endpoint can reuse it instead of duplicating the compose file check.

* docs(app-store): document orphaned stack directory cleanup behavior
2026-04-12 19:26:29 -04:00
Anso 4909c35e50 fix(resources): harden Resource Explorer with auth, validation, design, and UX fixes (#527)
- Sanitize error messages in all delete/prune/create/inspect endpoints
  to prevent Docker internals from leaking to the frontend
- Add CIDR, IPv4, and Docker resource ID input validation
- Add requirePaid gate to network topology endpoint
- Add invalidateNodeCaches after image/volume/network mutations
- Fix design system violations: card borders, destructive button variant,
  visible DialogDescription, overflow-auto replaced with ScrollArea,
  hardcoded Tailwind colors replaced with tokens
- Gate purge button behind isAdmin to prevent silent 403s
- Fix shared inspect loading state to be per-network-row
- Parse error response bodies for meaningful toast messages
- Add clipboard API fallback for non-HTTPS contexts
- Render Options section in network inspect sheet
- Add operational and diagnostic logging for resource operations
- Extend validation and DockerController test suites
- Update docs with Options field in network inspect
2026-04-12 15:31:35 -04:00
Anso d4882d32d9 fix(app-store): harden App Store with auth, validation, bug fixes, and design compliance (#523)
* fix(app-store): harden App Store with auth, validation, bug fixes, and design compliance

Add authMiddleware to GET /api/templates and POST /api/templates/deploy
endpoints. Add isValidStackName and isPathWithinBase checks to the deploy
endpoint. Replace fs.existsSync with async fsPromises.access. Extract
FileSystemService to local variable to avoid repeated getInstance calls.

Fix template mutation bug where PUID/PGID/TZ duplicated on re-open by
working on a copy instead of mutating state. Make env_file conditional
in generated compose YAML (only when env vars exist). Add port validation
(range 1-65535) with visual feedback and deploy blocking. Add env key
collision warning toast for custom variables.

Replace any types with proper LSIO API interfaces. Change catch types
from any to unknown with getErrorMessage. Add structured logging with
[Templates] prefix and diagnostic logging gated behind Developer Mode.

Align with design system: remove hardcoded bg-white and text-red-500,
use ScrollArea, fix destructive button variant, add tabular-nums and
strokeWidth 1.5, use cn() for conditional classes. Add empty-registry
state distinct from no-search-results.

Add 16 unit tests for TemplateService covering compose generation,
conditional env_file, env string generation, and cache clearing.
Update App Store docs with port validation and env collision details.

* refactor(app-store): remove unused interface exports

Remove export keyword from interfaces that are only used within their
own file: TemplateEnv, TemplateVolume, and TemplatesResponse in
TemplateService.ts; TemplateEnv and Template in AppStoreView.tsx.
No external consumers import these types.

* fix(app-store): remove unused fs default import

The fs.existsSync call was replaced with fsPromises.access in the
deploy endpoint, leaving the fs default import unused. Remove it
to fix the ESLint no-unused-vars error in CI.
2026-04-12 14:31:00 -04:00
Anso 9db97107aa fix(dashboard): harden real-time dashboard with bug fixes and design compliance (#517)
* fix(dashboard): harden real-time dashboard with bug fixes and design compliance

- Fix host alert spam: add 5-minute cooldown for CPU/RAM/disk threshold
  alerts, preventing duplicate notifications every 30s during sustained
  breaches. Extract shared dispatchWithCooldown helper (also used by
  Docker janitor alerts).
- Fix memory metric inflation: subtract filesystem cache from stored
  memory_mb values, matching the existing calculateMemoryPercent logic.
- Fix crash detection reliability: replace fragile 'seconds ago' string
  matching with a tracked Set of alerted container IDs. Containers are
  only alerted once per crash event, with automatic cleanup when they
  start running again or after a 1-hour TTL.
- Fix health status bar: exited containers now trigger 'degraded' state
  independently of unread error notifications.
- Fix CPU chart Y-axis: auto-scale when aggregate container CPU exceeds
  100% instead of silently clipping at the hardcoded domain ceiling.
- Fix grammar: 'actives' to 'active' in container count label.
- Add shadow-card-bevel to all dashboard cards per design system.
- Update dashboard docs to reflect revised health status thresholds.

* test(dashboard): update monitor service tests for new alert signatures

- Add container Id fields to crash detection test fixtures
- Update host alert assertions to match dispatchWithCooldown 3-arg call
- Fix unhealthy container test to use State: 'unhealthy' instead of
  State: 'running' (running containers are now skipped in crash detect)
2026-04-12 04:25:41 -04:00
Anso 023e962a26 fix(fleet): forward host bind mounts to self-update helper container (#509)
The self-update helper container runs `docker compose up -d
--force-recreate` to recreate the main Sencho container. Previously it
only mounted the docker socket, the compose working directory, and the
data directory. If the user's docker-compose.yml references env_file,
configs, or secrets at paths outside the compose working directory
(e.g. /opt/docker/env/globals.env), the helper could not resolve them
and compose failed with "env file not found".

Now during initialize(), SelfUpdateService collects all host bind mounts
from the container inspect data (filtered to Type=bind). In
triggerUpdate(), these are forwarded to the helper as read-only mounts
at their original host paths (source:source:ro), skipping the socket,
data dir, and compose working dir which are already mounted explicitly.
This lets docker compose resolve any host-path reference the user has
configured, without needing to parse compose files for specific
directives.
2026-04-12 02:20:42 -04:00
Anso 622c1f9262 feat: home dashboard and Settings Hub polish (#506)
* feat(dashboard): drop CPU column and relative timestamp from Stack Health and status bar

The Stack Health table's CPU column duplicated data already surfaced in
the top ResourceGauges and the CPU Usage historical chart. The health
status bar's 'just now' timestamp was cosmetic: no consumer relied on
lastUpdated state for polling, staleness detection, or conditional
rendering. Removing both tightens the dashboard and eliminates a dead
prop chain through useDashboardData.

* refactor: remove dead admin_email field from setup flow

The Setup form captured an admin email under 'Used for license recovery.
Never shared with third parties.' but the value was written to
global_settings and read nowhere: no license recovery, SMTP, or support
contact flow consumed it. Rather than building UI on top of the dead
field, delete the input, the payload key, and the backend persistence.
Any orphaned row from prior setups is harmless and the frontend ignores
unknown settings keys.

* feat(settings): use Radix ScrollArea with per-section scroll memory

Settings Hub used a native-scroll div that snapped to the top every time
the user switched subsections and exposed the default browser scrollbar.
Wrap the nav and content panes with the shadcn ScrollArea (Radix under
the hood, type='hover') and expose a viewportRef so the modal can stash
each section's scrollTop in a ref and restore it via useLayoutEffect on
switch. Style the thumb with translucent foreground tokens so it reads
as glass against popovers and dialogs. Replaces a hand-rolled scroll
hook and ad-hoc CSS utility.
2026-04-12 01:04:15 -04:00
Anso ba9c4f4aa6 fix(compose): move atomic backup out of stack folder, silence stale stats 404s (#498)
The Skipper/Admiral atomic deploy/update path used to create
.sencho-backup/ inside the user's stack folder, which silently failed
with EACCES whenever a container had chowned the bind mount (swag,
tautulli, linuxserver/* images, etc). That broke auto-rollback and the
manual rollback endpoint for those stacks. Stack backups now live under
<DATA_DIR>/backups/<stackName>/ next to sencho.db, which is always
writable by the Sencho user.

While stress-testing the same scenario, MonitorService also flooded the
error log with "Error parsing stats for container ... 404 no such
container" because per-container stats polls (30s tick) raced with
docker compose recreating containers. The 404 case is now skipped
silently; non-404 stats failures still log at error level.
2026-04-10 20:06:17 -04:00
Anso 3d69746eee fix(fleet): make local self-update flow reliable end-to-end (#472)
The "Updating Sencho..." overlay used to dismiss prematurely while the
image pull was still running, after which the local node card would get
stuck in "updating" and eventually surface a generic "Timed Out" error
while the container remained on the old version.

Three root causes are addressed:

1. The image pull was synchronous (`execFileSync`), which blocked the
   Node event loop. The overlay's health probe saw the server come back
   the moment the pull finished and reloaded the page, even though the
   container had not restarted yet. The pull is now async via
   `promisify(execFile)`, so /api/health and /api/fleet/update-status
   keep serving throughout.

2. The overlay reloaded on the first 200 from /api/health regardless of
   whether the underlying process had actually restarted. /api/health
   now exposes the gateway boot timestamp, and the overlay captures it
   pre-update and only reloads when it observes a different value. A
   wasOffline-then-online fallback handles the case where the pre-update
   fetch failed.

3. Helper container spawn errors from `docker run` were silently
   discarded, so a failed compose recreate never surfaced anywhere.
   Errors are now captured into `lastUpdateError` via the execFile
   callback and surfaced through the existing /api/fleet/update-status
   error path.

A 3-minute early-fail heuristic on the local node block surfaces a clear
failure message when the helper fails silently, instead of waiting the
full 5-minute timeout for an unknown failure.
2026-04-10 12:00:44 -04:00
Anso 368bef20d3 fix(fleet): detect updates via GitHub Releases instead of gateway self-comparison (#454)
* fix(fleet): detect updates via GitHub Releases instead of gateway self-comparison

The fleet update check compared each node's version against the gateway's
own version, so the local node could never appear outdated. Now fetches
the actual latest release from GitHub Releases API with a 30-minute
in-memory cache and thundering-herd protection. The Recheck button
invalidates this cache via ?recheck=true to force a fresh lookup.

* docs(fleet): update docs to reflect GitHub Releases version detection

Replace "Gateway version" references with "Latest version" to match the
new label. Document that version comparison uses the latest GitHub release
rather than the gateway's own version, and that Recheck refreshes the
cached latest version.
2026-04-09 12:21:37 -04:00
Anso cc23732727 docs(topology): update network topology docs with new features and screenshots (#450)
Document dagre auto-layout, enriched container nodes, click-to-logs,
and system network toggle. Refresh screenshots to show current UI.
2026-04-08 21:42:40 -04:00
Anso 6fff2c2d35 fix(fleet): resolve self-update compose file access and improve completion detection (#441)
The self-update feature failed on remote nodes because SelfUpdateService
ran `docker compose -f <host_path>` inside the container, where the host
compose file path does not exist. The fix splits the update into two
steps: (1) pull the latest image directly via `docker pull`, and (2)
spawn a short-lived helper container that mounts the compose directory
from the host and runs `docker compose up --force-recreate`.

Additional changes:
- Use execFileSync/execFile with argument arrays instead of shell strings
  to eliminate shell injection surface from Docker label values
- Add Signal 4 completion detection: mark update as completed when the
  remote version matches the gateway version (with 15s elapsed guard)
- Extend early failure heuristic from 90s to 3 minutes for slow pulls
- Distinguish "node unreachable" from "node lacks self-update capability"
  in error messages; use silent skip in update-all to avoid res crashes
- Add requireAdmin guard to POST /api/system/update
- Handle comma-separated compose config file paths (multiple -f flags)
- Update fleet docs with self-update mechanism, troubleshooting entries
2026-04-08 14:59:11 -04:00
Anso be7eda85f1 fix(billing): hide billing portal for lifetime licenses (#427)
Lifetime licenses have no recurring subscription, so the Lemon Squeezy
customer portal cannot generate a URL. The Manage Subscription button in
Settings already had the isLifetime guard, but the Billing button in the
profile dropdown did not, causing a confusing "No billing portal
available" error.

- Add !license.isLifetime guard to UserProfileDropdown (matches
  LicenseSection pattern)
- Move lifetime detection into getBillingPortalUrl() so the service owns
  all billing eligibility logic
- Change return type to { url } | { error } discriminated union for
  clear error propagation
2026-04-08 09:51:27 -04:00
Anso f6d2199978 feat(resources): add loading toast for prune, delete, and purge operations (#426)
Show a loading notification with spinner and indeterminate progress bar
while Resource Hub operations are in progress, replacing the dead moment
between confirmation and result.
2026-04-08 09:46:27 -04:00
Anso ca8f22734d fix(auto-update): proxy update execution to remote nodes via Distributed API (#419)
* fix(auto-update): proxy update execution to remote nodes via Distributed API

Remote auto-update policies previously failed because the scheduler tried
to access the Docker daemon directly on remote nodes. Now the scheduler
detects remote nodes and proxies the update execution via HTTP to the
remote Sencho instance's new /api/auto-update/execute endpoint, which
runs image checks and compose updates locally on the remote machine.

* test(auto-update): add getNode mock to NodeRegistry in scheduler tests

The executeUpdate method now calls NodeRegistry.getNode() to detect
remote nodes. The test mock for NodeRegistry was missing this method,
causing the two executeUpdate tests to fail.
2026-04-07 01:44:39 -04:00
Anso cc2da99d6f fix(fleet): resolve stuck update states and improve detection (#405)
* fix(fleet): resolve stuck update states and improve update UX

The fleet node update flow had several bugs: the in-memory update tracker
never cleared terminal states (timeout, failed, completed), leaving nodes
permanently stuck with no way to retry or dismiss. The Recheck button
only re-fetched stale state without clearing it, and the POST trigger
rejected retries with 409 even after timeout.

Backend fixes:
- Add DELETE endpoints (single node + batch) to clear tracker entries
- Fix 409 race: detect expired timeouts and clear terminal states before
  re-triggering
- Populate error messages in the tracker for timeouts and failures
- Include error field in the update-status API response
- Auto-expire completed entries after 60 seconds

Frontend fixes:
- Add retry (RotateCcw) and dismiss (X) buttons on failed/timed-out badges
- Show error details via animated cursor hover (CursorFollow pattern)
- Recheck button now batch-clears all terminal states before fetching
- Recheck shows loading spinner and disables while checking
- Extract NodeCardProps interface for readability

* fix(fleet): detect update completion via process start time

Remote nodes that cannot report their version (e.g. older builds)
caused updates to always time out because completion detection
relied solely on version comparison. The gateway now tracks the
remote node's process start time from /api/meta and detects
container restarts by comparing it across polls.

Also extracts a createTracker() factory to eliminate repeated
object construction across 5 call sites.

* docs: add troubleshooting for first-update timeout on old nodes

Adds a new troubleshooting entry explaining why the first remote
update on nodes running pre-v0.40.0 always times out (neither
version nor process start time can be detected). Documents the
fix: dismiss, recheck, and confirm the node updated.

Also adds a screenshot of the timed-out state with retry/dismiss
buttons to the remote updates feature page.

* fix(fleet): detect update completion via offline detection and error reporting

The update completion detection relied on version change and process
start time, both of which fail on nodes running older Sencho versions
that report "unknown" and lack the startedAt field. This caused every
update to time out after 5 minutes.

Add three-signal detection: version change, process restart (startedAt),
and offline/online detection (node went unreachable during update and
came back). Also add a 90-second early failure heuristic for when the
remote image pull fails silently, and surface pull errors from
SelfUpdateService via /api/meta so the gateway can report them
immediately.

* fix(deps): bump vite to 8.0.5 to resolve high severity vulnerabilities

Fixes GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583.

* fix(deps): bump vite in backend lockfile to resolve audit failures

Vitest pulls in vite as a transitive dependency. Bumps to 8.0.5.
2026-04-06 20:09:55 -04:00
Anso a55d1245f8 fix(fleet): resolve version detection pipeline for Docker builds (#402)
* fix(fleet): resolve version detection pipeline for Docker builds

The Dockerfile backend-builder stage was missing a COPY of the root
package.json, causing generate-version.js to fall back to "0.0.0-dev"
at build time. At runtime, the filesystem walk also failed (root
package.json not in the final image), producing the string "unknown"
which the frontend rendered as "vunknown".

Changes:
- Dockerfile: copy root package.json into backend-builder stage
- CapabilityRegistry: return null (not "unknown") for unresolvable
  versions; add isValidVersion() type guard; normalize remote meta
  responses to strip "unknown"/"0.0.0-dev" sentinel values
- Fleet endpoints: hoist gateway version validation outside per-node
  loops; treat unresolvable remote versions as "potentially outdated"
  instead of silently marking them up to date
- FleetView: guard all version display points (card badge, update
  button, gateway label, modal columns) via shared formatVersion()
- EditorLayout, CapabilityGate: use shared isValidVersion utility
- New frontend/src/lib/version.ts shared utility
- Docs: add troubleshooting section for version display edge cases
- Screenshots: updated Fleet Overview and Node Updates modal

* docs: update fleet node updates screenshot with live remote node
2026-04-06 03:03:56 -04:00
Anso f841c402b2 fix(licensing): resolve Admiral variant detection and lifetime license handling (#376)
* fix(licensing): resolve Admiral variant detection and lifetime license handling

The Lemon Squeezy variant name for Admiral licenses contains "Admiral"
(not "Team"), but getVariant() only checked for "team" and "personal".
This caused Admiral licenses to be misidentified as Skipper, locking all
Admiral-exclusive features.

- Map "admiral" variant names to internal "team" value, "skipper" to "personal"
- Add isLifetime field to LicenseInfo API response
- Hide "Manage Subscription" button for lifetime licenses (no billing portal)
- Show "Duration: Lifetime" instead of empty renewal date
- Hide upgrade cards for active Admiral users
- Add 23 unit tests covering variant resolution, tier computation, and lifetime detection
- Add troubleshooting entries for wrong tier label, locked features, and billing portal errors

* fix(licensing): address code review findings

- Fix nested ternary in LicenseSection JSX; restore conditional rendering
  to avoid showing an empty "N/A" row for non-subscription states
- Clean up test file: use shared svc variable, remove redundant comments,
  add trialDaysRemaining assertions, rename describe block
2026-04-05 07:00:21 -04:00
Anso f516275834 refactor(licensing): replace Pro branding with Community/Skipper/Admiral tiers (#375)
Eliminate all references to "Pro" across backend, frontend, and docs.
Internal tier value renamed from 'pro' to 'paid'; user-facing text now
uses the thematic tier names (Community, Skipper, Admiral).

- Rename LicenseTier 'pro' to 'paid' in backend and frontend types
- Rename requirePro guard to requirePaid, error code PRO_REQUIRED to PAID_REQUIRED
- Rename ProGate.tsx to PaidGate.tsx with updated copy
- Fix: trial users can now see upgrade/purchase cards in Settings
- Update all docs and openapi.yaml to use correct tier names
2026-04-05 05:59:36 -04:00
Anso a1804c8fbe docs: comprehensive review and refresh of all documentation (#374)
* docs: comprehensive review and refresh of all documentation pages

Reviewed every doc page against the current app state after the v0.38
dashboard redesign. Updated content, fixed inaccuracies, and refreshed
all screenshots at 1920x1080.

Pages updated:
- introduction: expanded feature list to 25 items across 6 subsections
- quickstart: fixed docker run command (Docker Hub, auto JWT, COMPOSE_DIR)
- configuration: replaced personal paths with generic /home/user/docker
- sso-quickstart: fixed Settings navigation reference
- sso: added SSO_LDAP_DISPLAY_NAME env var
- overview: added 8 missing feature sections (labels, API tokens, schedules, etc.)
- dashboard: complete rewrite for new health bar, gauges, stack health table
- stack-management: updated for UP/DN indicators, rollback button, split actions
- editor: rewritten for two-column layout, inline stats, embedded terminal
- resources: updated Quick Clean docs, added network topology and inspect
- app-store: updated categories, deploy sheet details, permission gate, settings
- openapi.yaml: fixed YAML parsing error on line 1831

Screenshots refreshed: 14 images across 6 feature areas.

* docs: review and update observability, console, multi-node, and compatibility pages

- Global Observability: fix log format fields, add download button docs,
  split display limits into memory buffer vs rendered rows, correct
  settings labels
- Host Console: remove internal implementation details per security
  docs policy, add stack directory behavior, expand header bar docs,
  remove unverified scrollback claim
- Multi-Node: add Compose Directory field, document connection test
  details panel, fix edit/delete node behavior, simplify token security
  section, remove internal details
- Node Compatibility: add missing self-update capability, remove
  internal endpoint paths and cache TTL, move from Features to
  Reference group in navigation
- Refresh all screenshots for the redesigned UI (7 images)

* docs: review and refresh fleet, remote updates, labels, alerts, routing, and webhooks pages

- Fleet View: added node updates modal, container detail, version/update/critical badges, Tags filter
- Remote Updates: removed internal details, added capability cross-link, fast polling
- Stack Labels: three creation methods, two assignment methods, 10 colors, bulk actions screenshot
- Alerts & Notifications: fixed metric labels, added notification popover detail, status banner
- Notification Routing: HTTPS requirement, rule card layout, channel terminology fix
- Webhooks: corrected license tier to Admiral, matched action labels to UI, removed internal security details, added local-only note
- Troubleshooting: centralized entries from remote-updates, stack-labels, notification-routing
- Refreshed all screenshots at 1920x1080, removed 11 orphaned images

* docs: review and refresh RBAC, user management, and atomic deployments pages

- RBAC: added missing Auditor role (5th role), updated permission matrix, fixed license tier references, documented username/password validation rules, self-deletion protection
- Atomic Deployments: added "Which operations are protected" section covering webhooks/schedules/app store, removed internal backup path, fixed license tier to Skipper/Admiral
- Screenshots: cropped to dialog element per updated strategic cropping guideline, removed 2 orphaned images

* docs: review and refresh fleet-wide backups and audit log pages

Update fleet-backups page to reflect current inline create form, add
scheduled snapshots section, document the detail view and restore dialog,
expand RBAC table to all five roles. Update audit log page to document
expanded row detail fields, pagination, refresh button, and data retention
screenshot. Replace all screenshots with fresh captures at 1920x720.

* docs: review and refresh API tokens and private registries pages

- API Tokens: clarify Full Admin scope, add Managing tokens section
  with card details, document revocation confirmation dialog, add
  usage tracking to security model, refresh screenshot
- Private Registries: add Managing registries section with card details
  and action buttons, document edit behavior, fix URL auto-fill
  description, remove encryption algorithm name per security policy,
  fix grammar, refresh both screenshots

* docs: review and refresh auto-update policies, scheduled operations, and SSO pages

- Auto-Update Policies: document all 8 table columns, expand action buttons,
  add "All Stacks" wildcard option, fix field labels, add CSV export and
  pagination details, refresh screenshots
- Scheduled Operations: fix System Prune target description, add Task List
  table columns, restructure create dialog fields with action-specific
  annotations, rewrite execution history with column table, refresh screenshots
- SSO: remove encryption algorithm name per security policy, add LDAP and
  OIDC configuration field tables, document provider card controls (Save,
  Test Connection, Remove, Active badge), refresh screenshots
- Move SSO troubleshooting entries to centralized troubleshooting page

* docs: review and refresh licensing & billing page

Update upgrade card feature lists to match actual tier gating (Skipper:
fleet view, webhooks, labels, atomic deployments, backups, auto-update
policies; Admiral: scoped RBAC, SSO, audit log, host console, API
tokens, private registries, scheduled operations). Add flex layout to
align upgrade card buttons at the bottom. Replace stale screenshot with
fresh community and active license captures. Add feature breakdown
subsection and profile menu billing shortcut to docs.

* docs: review and refresh settings reference and security advisories pages

Settings Reference: add 5 missing sections (SSO, API Tokens, Registries,
Labels, Routing), expand Users from 2 to 5 roles, fix System Limits and
Developer field labels to match UI, restructure Developer into Streaming
and Data Retention sub-tables, update App Store and Support sections,
refresh overview screenshot.

Security Advisories: restructure into versioned sections (v0.25.x
hardening and v0.19-v0.24 CVE remediation), expand from 3 bullet points
to 10 specific improvements, fix GitHub URL from SaelixCode to AnsoCode,
redact internal details per security docs policy.

Remove "Sencho Pro" product name from all three pages, replaced with
tier names (Community, Skipper, Admiral).

* docs: review and refresh troubleshooting page, remove architecture and development guides

- Rewrote forgotten password section to remove exposed SQL and table names
- Updated all Settings navigation paths to Profile > Settings > X
- Fixed network topology from "tab" to "view mode", added Pro license note
- Updated Prune Networks to current "Prune Dead Networks" label
- Corrected update check cooldown from vague to 2 minutes
- Consolidated two network creation error sections into one
- Removed hardcoded version reference (v0.34.0)
- Replaced em dashes throughout
- Deleted architecture.mdx (exposes internal implementation details)
- Deleted development.mdx (contributor guide belongs in repo, not public docs)
- Removed both pages from docs.json navigation

* docs: review and refresh operations pages (backup, upgrade, self-hosting, troubleshooting)

Backup & Restore:
- Added missing encryption.key to all backup/restore procedures
- Added Warning about restoring db without matching encryption key
- Added cross-reference to Fleet-Wide Backups for paid tiers
- Removed false claim about no built-in backup scheduler
- Updated cron example to include encryption key copy

Upgrading Sencho:
- Removed internal migration details (table names, column specs, encryption algorithm)
- Replaced with high-level migration summary per security docs policy
- Added encryption.key to pre-upgrade backup command
- Updated version pinning example from 0.25.3 to 0.38.0
- Added Remote Updates cross-reference for Skipper/Admiral users

Self-Hosting Best Practices:
- Removed JWT_SECRET from env var table (auto-generated, not an env var)
- Removed PORT from env var table (hardcoded to 3000, not configurable)
- Added API_RATE_LIMIT to env var table (actually exists in code)
- Fixed listen port description from "configurable" to "fixed"
- Updated resource recommendations based on measured footprint audit
- Removed su-exec reference (internal implementation detail)
- Upgraded data directory Note to Warning with file names

Troubleshooting:
- Fixed "Pro features" heading to "Paid features" with correct tier names
2026-04-04 21:32:51 -04:00
Anso 55d3b8ca1d feat(stacks): state-aware sidebar context menu and Open App action (#368)
* feat(stacks): state-aware sidebar context menu and Open App action

- Context menu now adapts to stack state: running stacks show
  Stop/Restart/Update, stopped stacks show Deploy only
- Added "Open App" shortcut to open a stack's web UI directly
  from the sidebar (visible when running with a published port)
- Backend bulk status endpoint enriched with mainPort detection
- Reduced manual image update check cooldown from 10 to 2 minutes
- Rate limit error message now derives from the configured constant

* fix(stacks): use const for bulkPorts (prefer-const lint)
2026-04-03 21:41:01 -04:00
Anso 6c26ae3f50 feat(license): distributed license enforcement across multi-node setups (#359)
* feat(license): distributed license enforcement across multi-node setups

The primary instance's license tier is now asserted to remote nodes on
every proxied HTTP and WebSocket request via trusted headers. Remote
nodes honor the assertion only when the request carries a valid
node_proxy JWT, preventing unauthorized elevation from browsers or API
tokens. Falls back to local license tier for direct access.

* fix(test): remove unused vi import in distributed-license tests
2026-04-03 11:31:14 -04:00
Anso 87b5908288 feat(fleet): add remote node update management (#353)
Add the ability to check for outdated nodes and trigger over-the-air
updates from Fleet View. Nodes self-update by pulling the latest Docker
image and recreating their container via the "last breath" pattern.

Backend:
- SelfUpdateService: self-container identification via HOSTNAME + Docker
  Compose labels, triggers pull + force-recreate
- CapabilityRegistry: runtime capability disabling via disableCapability()
- POST /api/system/update (202 + deferred self-update)
- GET /api/fleet/update-status (version comparison across fleet)
- POST /api/fleet/nodes/:nodeId/update (single node)
- POST /api/fleet/update-all (bulk remote update)
- In-memory update tracker with 5-min timeout

Frontend:
- Node Updates modal with summary stats, search filter, table layout,
  per-node Update buttons, and bulk Update All
- Version badges and update-available indicators on node cards
- ReconnectingOverlay for local node updates (polls /api/health)
- 5s fast-poll when any node is actively updating
- UpdateStatusBadge shared component for consistent badge rendering

Requires Skipper (Pro) tier. Nodes must be deployed via Docker Compose
with Docker socket access.
2026-04-03 01:39:22 -04:00
Anso ee75811e25 feat(nodes): add capability-based node compatibility negotiation (#350)
* feat(nodes): add capability-based node compatibility negotiation

Each Sencho instance now exposes /api/meta with its version and supported
capabilities. When the user switches nodes, the frontend fetches this
metadata and disables features the remote node doesn't support via a
CapabilityGate overlay. Version is shown in the node switcher dropdown
and connection test results.

- Backend: CapabilityRegistry with static capability list and fetchRemoteMeta helper
- Backend: /api/meta (public) and /api/nodes/:id/meta (auth) endpoints
- Frontend: NodeContext enhanced with per-node meta caching (5min TTL)
- Frontend: CapabilityGate component with typed Capability union
- Frontend: 13 features wrapped with capability gates
- Docs: node-compatibility.mdx + OpenAPI spec updates

* fix(nodes): revert to require() for package.json version reading

The static import fails in the Docker multi-stage build because the
root package.json is not copied into the backend-builder stage. The
require() call resolves at runtime when the file is available.
2026-04-03 00:06:34 -04:00
Anso 1b573f542a feat(notifications): add shared notification routing rules (Admiral tier) (#347)
Route stack alerts to specific Discord, Slack, or webhook channels instead
of the single global endpoint. Includes per-rule enable/disable, priority
ordering, and automatic fallback to global agents when no rule matches.

- Add notification_routes table, interface, and CRUD in DatabaseService
- Add routing logic in NotificationService.dispatchAlert with optional stackName
- Pass stack context from MonitorService (crash/health) and SchedulerService
- Add 5 API endpoints gated with requireAdmin + requireAdmiral
- Add NotificationRoutingSection UI with Combobox stack picker, channel tabs
- Parallel webhook dispatch via Promise.allSettled
- 10 unit tests covering routing, fallback, and edge cases
- Documentation with screenshots at docs/features/notification-routing.mdx
2026-04-02 22:22:27 -04:00