Commit Graph

891 Commits

Author SHA1 Message Date
Anso ed553f1f19 feat: change default listen port from 3000 to 1852 (#756)
Updates the backend listen port, Vite dev proxy target, Docker EXPOSE,
compose port mapping, .env.example default, GitHub Actions smoke-test
default, healthcheck URLs, and every doc/example reference. Test fixtures
that include example URLs were updated for consistency, though their
assertions are port-agnostic.

The rate-limit value of 3000 in middleware/rateLimiters.ts and the
3000 entry in WEB_UI_PORTS (which detects user containers like Grafana)
are intentionally untouched.
2026-04-24 22:23:31 -04:00
Anso d6b744e8e6 feat(license): replace local auto-trial with Lemon Squeezy hosted trial flow (#755)
Fresh installs land on the Community tier. The 14-day Admiral trial is now
issued by Lemon Squeezy via their hosted checkout: the user enters email +
card, receives a license key by email, and pastes it into the existing
Settings > License activation field.

Backend changes:
- LicenseService.initialize() no longer auto-creates a license_status='trial'
  row on first boot. It now only ensures an instance_id exists and starts
  periodic validation.
- Drop the TRIAL_DURATION_DAYS constant.
- Drop the status='trial' early-return in getVariant() so LS-issued trials
  resolve through the normal variant metadata path (variant_name / product_name).
- Trial branches in getTier() and getLicenseInfo() are retained for future
  work that may detect trial state from Lemon Squeezy metadata; they are
  currently unreachable via the Sencho code paths.

Frontend changes:
- Settings > License surfaces a new "Try Admiral free for 14 days" CTA block
  with Start monthly trial and Start annual trial buttons that open Lemon
  Squeezy hosted checkout. The CTA is visible only when the user has no paid
  access and is not already on a trial.
- Reserve the Admiral upgrade card for the Skipper-active upgrade path so
  unlicensed users see one Admiral path (the trial CTA) instead of two.
- Pull the inline Lemon Squeezy checkout URLs into named module constants so
  the Skipper, Admiral monthly, and Admiral annual endpoints are defined in
  one place.

Test changes:
- license-service.test.ts covers the no-auto-trial startup path and updates
  the trial-variant test to reflect the metadata-driven resolution.
- afterAll in the initialize() describe block calls destroy() so the 72-hour
  validation interval does not leak into sibling test files.

Docs:
- Rewrite the Free trial section in features/licensing.mdx to document the
  new LS checkout flow (email + card required, auto-converts on day 14 unless
  cancelled).
- Add an operations/troubleshooting entry for cases where the trial license
  key email does not arrive.
2026-04-24 16:26:36 -04:00
Anso a502da54ee feat(sso): split SSO providers by delivery model across tiers (#754)
Custom OIDC stays on Community so self-hosters can wire any spec-compliant
OIDC identity provider (Authelia, Keycloak, Authentik, Zitadel, and others).
Google, GitHub, and Okta one-click presets move to Skipper. LDAP / Active
Directory and scoped RBAC are Admiral-only.

Backend enforces the split via a new requireTierForSsoProvider helper in
middleware/tierGates.ts, applied after requireAdmin in all four ssoConfig
mutation handlers. GET /sso/config (list) stays ungated so downgraded
admins can still see previously-configured providers. Invalid provider ids
now 400 before the tier check to avoid leaking tier information.

Frontend adds a compact mode to PaidGate and AdmiralGate for inline
list-item locks, and SSOSection reorders the provider cards as
Custom OIDC > Google > GitHub > Okta > LDAP to reinforce the
free-to-paid progression.

Stale 'SSO is Admiral' copy in AdmiralGate, PaidGate, and the Admiral
upgrade card on the License settings page has been replaced to reflect the
new split. User-facing licensing, SSO, overview, quickstart, and security
docs have been updated with the per-tier provider matrix.
2026-04-24 15:48:03 -04:00
Anso 3a20e37625 docs(backend): strip stale phase annotations from canonical-order comment (#753)
The canonical middleware-order comment in app.ts carried historical
notes from the index.ts refactor ("before Phase 4 finishes", "moves to
routes/* in Phase 4", "moves here in Phase 5") that are no longer
active-voice descriptions of current state. Replace with plain
descriptions matching the final module layout. The 16-step enumeration
and the invariant paragraph about public routers (metaRouter, authRouter,
mfaRouter, ssoRouter) sitting before the auth gate are preserved.

No behavior change.
2026-04-24 10:21:25 -04:00
Anso 43a595905b fix(backend): restore remote proxy mount order before local routers (#747)
The index.ts refactor inverted the proxy mount order. The pre-refactor
monolith mounted `app.use('/api/', remoteNodeProxy)` before any inline
route, so remote-nodeId requests short-circuited into the proxy. After
the refactor the proxy was registered after every per-group router, so
Express matched local routers first and remote-nodeId requests were
silently handled with the control instance's local state (e.g.
GET /api/stacks with x-node-id=<remote> returned local stacks rather
than the remote's).

Fix moves createRemoteProxyMiddleware() between enforceApiTokenScope
and the first per-group router, matching middleware-order.md step 13
and restoring pre-refactor behavior. PROXY_EXEMPT_PREFIXES continues to
cover gateway-level paths (auth, nodes, license, fleet, webhooks, meta)
that must stay local even when x-node-id targets a remote.

Add four regression guards that would have caught this:

- json-parser-bypass.test.ts: asserts conditionalJsonParser leaves the
  request stream intact on proxy-eligible paths so http-proxy can pipe
  the raw body to the upstream; spins up a local echo server and
  verifies the bytes arrive.
- proxy-mount-order.test.ts: asserts a remote-nodeId GET short-circuits
  into the proxy (502 from unreachable upstream) instead of matching a
  local router (200 from local state).
- upgrade-order.test.ts: pins WebSocket dispatch order by observing
  handler-specific side effects for notifications, remote forwarder,
  logs, and pilot tunnel.
- remote-console-session.test.ts: asserts the HTTP console-token route
  mints a JWT with the same claim shape as the shared mintConsoleSession
  helper, so gateway and WS forwarder tokens remain interchangeable.

Full suite: 73 files, 1,358 tests, all passing.
2026-04-24 10:20:08 -04:00
Anso 86722fe98e chore: gitignore docs/internal for engineering documentation (#746)
Reserve docs/internal/ for internal engineering documentation that should
not ship on docs.sencho.io. Mintlify's docs.json does not reference
internal/, so the folder is also invisible to the published site even if
it were tracked. The .gitignore entry prevents accidental commits.

The folder holds architecture deep-dives, module responsibilities,
shared-state inventory, WebSocket dispatch order, and refactor history
that are load-bearing for engineers but not appropriate for end-user
documentation.
2026-04-24 08:13:54 -04:00
Anso e9fce15010 refactor(backend): extract bootstrap into startup/shutdown modules (phase 5) (#745)
Move the startup and shutdown lifecycles out of index.ts:
- bootstrap/startup.ts exports startServer(server) - migration check,
  service initialization, background watchdogs, HTTP listen, pilot-agent
  loopback bind.
- bootstrap/shutdown.ts exports installShutdownHandlers(server) -
  SIGTERM/SIGINT handlers, in-order service stop chain, 10s force-exit
  guard, SQLite close.

Restructure MfaService to add an instance + lifecycle so the replay
purge timer no longer lives as a module-scope setInterval in index.ts.
MfaService keeps all existing static methods (generateSecret, verifyTotp,
currentWindow, generateBackupCodes, hashBackupCodes, verifyBackupCode,
formatBackupCodeForDisplay, normalizeBackupCode, buildOtpauthUri) so
every existing caller stays unchanged. The new start() / stop() pair
is idempotent and calls .unref() so test shutdown is not blocked.

bootstrap/startup calls MfaService.getInstance().start().
bootstrap/shutdown calls MfaService.getInstance().stop().

index.ts drops from 305 to 147 lines and now contains only the Express
app composition: createApp, route mounts, remote proxy, createServer,
attachUpgrade, static/SPA fallback, errorHandler, installShutdownHandlers,
and the require.main guard that boots the server when run directly.

Behavior is byte-for-byte identical: shutdown service order, log
strings, force-exit timer, pilot-agent loopback logic, and the MFA
purge cadence and debug logging all preserved verbatim.
2026-04-23 23:44:00 -04:00
Anso 155a231aae refactor(backend): extract stacks router (phase 4c-6, final route extraction) (#744)
Move the 17 /api/stacks/* endpoints out of index.ts into routes/stacks.ts.
Endpoints covered:
- list, statuses (bulk-status cache via CacheService)
- get / put stack compose content
- envs (resolve), env read, env write (multi env_file aware)
- create (plain + from-git with policy gate + optional deploy)
- delete (three-stage Docker-down, FS-delete, DB cleanup)
- containers list, services list
- lifecycle: deploy / down / restart / stop / start
- update-preview, update, rollback (Skipper+), backup info

The inline resolveAllEnvFilePaths helper moves with the router as a
file-local function. Handlers moved verbatim; middleware chains,
response shapes, and error messages preserved.

Removes twenty-two now-unused imports from index.ts: DockerController,
ComposeService, path, UpdatePreviewService, CacheService, GitSourceService,
GitSourceError, gitRepoHost, sendGitSourceError, STACK_STATUSES_CACHE_TTL_MS,
requirePermission, requirePaid, buildPolicyGateOptions, runPolicyGate,
triggerPostDeployScan, getTerminalWs, invalidateNodeCaches, getErrorMessage,
enforcePolicyPreDeploy, isValidStackName, isPathWithinBase, YAML.

index.ts drops from 1021 to 305 lines. All /api/* route groups now live
in routes/*.ts. index.ts contains only wiring (createApp, createServer,
attachUpgrade, route mounts, remote proxy, static serving, error handler)
and startup/shutdown lifecycles. Bootstrap extraction follows in phase 5.
2026-04-23 23:31:29 -04:00
Anso 3995086872 refactor(backend): extract nodes router (phase 4c-5) (#743)
Move the nine /api/nodes/* endpoints out of index.ts into routes/nodes.ts
(list, scheduling-summary, get, create, pilot-enroll, update, delete,
test, meta). mintPilotEnrollment and the REMOTE_META_* constants move
with the router as local helpers.

Handlers moved verbatim. Two safe cleanups applied during the move:
- Inline req.apiTokenScope 403 blocks replaced with the shared
  rejectApiTokenScope helper; payload shape unchanged.
- catch (error: any) rewritten to catch (error: unknown) with explicit
  instanceof Error narrowing to satisfy the no-any strictness rule.
  Response body shapes unchanged.

Removes now-unused imports from index.ts: jwt, crypto, authMiddleware,
isValidRemoteUrl, PilotTunnelManager, PilotCloseCode, CAPABILITIES,
getSenchoVersion, fetchRemoteMeta, RemoteMeta, FleetUpdateTrackerService,
plus the module-scope updateTracker alias.

index.ts drops from 1364 to 1021 lines. Only the stacks group remains
inline for the final phase 4c slice.
2026-04-23 23:16:58 -04:00
Anso d98b61cbca refactor(backend): extract container and port routers (phase 4c-4) (#742)
Move the six /api/containers/* and /api/ports/in-use endpoints out of
index.ts. Handlers moved verbatim. routes/containers.ts exports two
routers:
- containersRouter (mounted at /api/containers): list, stream logs,
  start, stop, restart.
- portsRouter (mounted at /api/ports): /in-use host port inventory.

Removes the now-unused requireAdmin import from index.ts. Middleware
chains, response shapes, and error messages are all preserved.

index.ts drops from 1437 to 1364 lines. Remaining inline groups:
stacks and nodes.
2026-04-23 23:06:26 -04:00
Anso 1a6ae8309d refactor(backend): extract security router (Trivy, scans, SBOM, policies, suppressions, compare) (phase 4c-3) (#741)
Move the entire /api/security/* surface out of index.ts:
- Trivy lifecycle: status, install, uninstall, update-check, update, auto-update toggle
- Scanning: POST /scan (image), POST /scan/stack (compose)
- Scan queries: list, get, vulnerabilities, secrets, misconfigs, image-summaries, SARIF export
- SBOM generation
- Scan policies CRUD (fleet-replicated; replica writes rejected)
- CVE suppressions CRUD (fleet-replicated; replica writes rejected)
- GET /compare: diff two scans

Handlers moved verbatim. Local CVE_ID_RE, parseScannersInput, and
shapeScanForResponse helpers move with the router. The previously-closure
fetchAll inside the SARIF handler is hoisted to module scope as
fetchAllPages so it is not re-allocated per request.

Removes nine now-unused imports from index.ts (parsePolicyEvaluation,
VulnerabilityScan, FleetSyncService, requireAdmiral, trivyInstallLimiter,
SbomFormat, TrivyInstaller, validateImageRef, applySuppressions,
generateSarif).

index.ts drops from 2130 to 1437 lines. Remaining inline groups:
containers, stacks, and nodes.
2026-04-23 22:50:37 -04:00
Anso 8ef8ce06ec refactor(backend): extract registries, system-maintenance, templates routers (phase 4c-2) (#740)
Move three more Round C route groups out of index.ts:
- /api/registries/* -> routes/registries.ts
- /api/system/{orphans,prune,docker-df,resources,images,volumes,networks,...} -> routes/systemMaintenance.ts
- /api/templates/* -> routes/templates.ts

Handlers moved verbatim. Middleware chains, response shapes, and error
messages are preserved. Registry scope-denial now uses the shared
rejectApiTokenScope helper (same payload shape).

index.ts drops from 2739 to 2130 lines. Remaining inline groups:
containers, stacks, security (Trivy/scans/SBOM/policies), and nodes.
2026-04-23 22:28:04 -04:00
Anso ba2cf99aa6 refactor(backend): extract auto-heal, notifications, console, sso-config routers (phase 4c-1) (#739)
Move four small Round C route groups out of index.ts:
- /api/auto-heal/* -> routes/autoHeal.ts
- /api/notifications/*, /api/notification-routes/* -> routes/notifications.ts
- /api/system/console-token -> routes/console.ts
- /api/sso/config/* -> routes/ssoConfig.ts

Share NOTIFICATION_CHANNEL_TYPES, cleanStackPatterns, and validateHttpsUrl
via a new helpers/notificationChannels.ts so agents.ts and notifications.ts
consume the same allowlist and URL validator.

Handlers moved verbatim. Middleware chains and response shapes preserved.
index.ts drops from 3231 to 2739 lines.
2026-04-23 22:10:41 -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
Anso f6a7898798 refactor(backend): add route tests then extract settings, scheduled-tasks, agents (phase 4b) (#737)
Round B of Phase 4. Writes integration tests for three under-covered
route groups BEFORE extracting them, then does the extraction once the
new tests pass against the monolith. index.ts drops from ~4,206 to
~3,678 lines.

New test coverage (42 new assertions):
- settings-routes.test.ts (14) — auth, admin gating, private-key stripping,
  allowlist, single-key write, bulk PATCH validation + partial update
- scheduled-tasks-routes.test.ts (18) — list/create/get/toggle/delete/runs,
  action+target_type matrix, cron validation, tier gating on non-admin
- agents-routes.test.ts (10) — GET/POST, admin gating, channel type +
  HTTPS URL validation, boolean enabled check, upsert semantics

Each suite was verified against the inline monolith first, then the
route extraction was performed byte-for-byte and all suites re-run to
ensure no regression.

New route files:
- routes/settings.ts — GET/POST/PATCH with PRIVATE_SETTINGS_KEYS strip,
  ALLOWED_SETTING_KEYS allowlist, and SettingsPatchSchema zod bulk schema
- routes/scheduledTasks.ts — 9 endpoints (list, create, get, update,
  delete, toggle, run-now, runs history, runs CSV export). File-local
  helpers parseTaskId, validateActionTarget, validateOptionalFields
  collapse duplication across create+update handlers. Uses shared
  escapeCsvField from utils/csv.ts.
- routes/agents.ts — notification-channel GET/POST. Owns
  NOTIFICATION_CHANNEL_TYPES and validateHttpsUrl locally because the
  notification-routes block still inlines identical copies; the helpers
  will converge once those routes extract in a later slice.
2026-04-23 21:22:39 -04:00
Anso 90eae03922 refactor(backend): extract webhooks, users, git-sources, and fleet routers (phase 4a-3) (#736)
Final slice of Phase 4 Round A. Pulls the four remaining well-tested route
groups out of index.ts. index.ts drops from ~5,930 to ~4,206 lines.

New route files:
- routes/webhooks.ts: /api/webhooks CRUD + HMAC-authenticated trigger.
  Uses shared webhookTriggerLimiter. Trigger preserves the raw-body path
  established by the conditional JSON parser for HMAC validation.
- routes/users.ts: /api/users CRUD + /:id/mfa/reset + /:id/roles
  scoped-assignment surface. Uses rejectApiTokenScope across every
  handler, validateUsername helper, BCRYPT_SALT_ROUNDS, and
  isSqliteUniqueViolation for the role-assignment UNIQUE guard.
- routes/gitSources.ts: /api/git-sources + /api/stacks/:name/git-source/*.
  Exports two routers (gitSourcesRouter + stackGitSourceRouter) because
  the per-stack paths need to mount at /api/stacks alongside the label
  routes extracted in phase 4a-1. String length limits are now named
  constants so the 400 responses stay truthful if the bounds change.
- routes/fleet.ts: /api/fleet role, sync, overview, node drill-down,
  update-status + trigger (single + fleet-wide), and snapshot CRUD +
  restore. Local parseIdParam helper collapses seven copies of the
  parseInt/isNaN route-param pattern.

Bugs fixed during review:
- users.ts :id/roles POST — replace the fragile
  (err as Error).message?.includes('UNIQUE constraint') check with
  isSqliteUniqueViolation from utils/errors.ts.

index.ts carries forward three symbols (updateTracker alias,
CVE_ID_RE, parseScannersInput) until the corresponding security /
nodes / scan routes get extracted in a later slice.
2026-04-23 21:05:04 -04:00
Anso b329916a0c refactor(backend): extract auth/MFA/SSO routers from index.ts (phase 4a-2) (#735)
Second slice of Phase 4. Pulls the three auth-family route groups out of
index.ts into focused routers. All handlers move verbatim; index.ts drops
~845 lines.

New route files:
- routes/auth.ts: /api/auth core (status, setup, login, password, logout,
  check, generate-node-token)
- routes/mfa.ts: /api/auth/login/mfa + full /api/auth/mfa/* surface
  (status, enroll start/confirm, disable, backup-codes/regenerate,
  sso-bypass)
- routes/sso.ts: /api/auth/sso/{providers,ldap,oidc/:provider/authorize,
  oidc/:provider/callback} + getSSOBaseUrl helper. Module-load calls
  SSOService.getInstance().seedFromEnv() so env-seeded providers are
  available on the first request.

Shared lifts:
- helpers/constants.ts: MFA_REPLAY_TTL_MS + MFA_REPLAY_PURGE_INTERVAL_MS
  (used by mfa.ts and the startup purge timer in index.ts) and
  BCRYPT_SALT_ROUNDS (shared between setup and password-change handlers).
- middleware/auth.ts: new reissueSessionAfterTokenBump(req, res, userId)
  helper collapses three copies of "bump → fetch user → re-sign cookie"
  across auth.ts (password change) and mfa.ts (enrol confirm, disable).

Code review fixes:
- File-local requireEnrolledMfaUser helper in mfa.ts eliminates four
  copies of "auth check + rejectApiTokenScope + load enrolled MFA" with
  near-identical shape.
- Applied BCRYPT_SALT_ROUNDS to auth.ts setup + password handlers.

Mount order in index.ts: authRouter / mfaRouter / ssoRouter sit before
authGate because login / setup / SSO-callback are public; handlers that
need auth use authMiddleware directly on the route.
2026-04-23 20:43:32 -04:00
Anso 50e64b058b refactor(backend): extract 8 low-blast-radius route groups into routers (phase 4a-1) (#734)
First slice of Phase 4 (route extraction). Pulls 8 well-tested, mostly
independent route groups out of index.ts into focused Router files. No
behavior change; every handler body moves verbatim.

New route files under backend/src/routes/:
- meta.ts            /api/health, /api/meta (mounted before authGate)
- license.ts         /api/license/* + /api/system/update,
                     exports scheduleLocalUpdate for the fleet route
- permissions.ts     /api/permissions/me
- convert.ts         POST /api/convert
- alerts.ts          /api/alerts/*
- labels.ts          /api/labels/* + PUT /api/stacks/:name/labels
                     (exported as stackLabelsRouter)
- apiTokens.ts       /api/api-tokens/*
- auditLog.ts        /api/audit-log/*

Shared helper lifts:
- helpers/cacheInvalidation.ts: invalidateNodeCaches()
- middleware/tierGates.ts: requireBody (was inline in index.ts)
- utils/errors.ts: isSqliteUniqueViolation (was inline in index.ts)
- middleware/apiTokenScope.ts: rejectApiTokenScope() helper (new)
- utils/csv.ts: escapeCsvField() (new)

index.ts drops from ~7520 to ~6775 lines and now mounts the routers right
after enforceApiTokenScope. The remote proxy and fleet/auth/webhooks/users
routes remain inline in index.ts pending later Phase 4 slices.

Code review fixes: rejectApiTokenScope helper replaces duplicated
`if (req.apiTokenScope) 403 SCOPE_DENIED` blocks in apiTokens.ts and
license.ts; escapeCsvField replaces the inline CSV escape in auditLog.ts.
2026-04-23 20:24:37 -04:00
Anso dc3699189d refactor(backend): extract remote proxy, WebSocket upgrade handler, and server factory (phase 3) (#733)
Phase 3 of the index.ts refactor. Pulls the remote HTTP/WS proxy plumbing,
the WebSocket upgrade dispatcher, and the http/WSS construction out of the
monolith. index.ts drops roughly 620 lines.

New modules:
- proxy/websocketProxy.ts: shared httpProxy.createProxyServer singleton
  (used by both the HTTP proxy middleware and the remote WS forwarder)
- proxy/remoteNodeProxy.ts: createRemoteProxyMiddleware() factory; consumes
  the isProxyExemptPath helper instead of open-coding the prefix list
- server.ts: createServer(app) returns { server, wss, pilotTunnelWss }
- services/FleetUpdateTrackerService.ts: singleton wrapping the in-flight
  fleet update tracker Map with create()/resolve() helpers
- helpers/consoleSession.ts: mintConsoleSession(), isConsoleSessionScope()
- websocket/upgradeHandler.ts: attachUpgrade(server, deps) dispatcher that
  runs the manual cookie/JWT verify and delegates to sub-handlers
- websocket/pilotTunnel.ts: handlePilotTunnel (pilot_enroll consumption and
  pilot_tunnel registration)
- websocket/notifications.ts: /ws/notifications local subscriber
- websocket/remoteForwarder.ts: remote-node WS proxy with console_session
  token exchange for interactive paths
- websocket/logs.ts: /api/stacks/:name/logs supervisor stream
- websocket/hostConsole.ts: /api/system/host-console PTY, Admiral-gated
- websocket/generic.ts: /ws exec + streamStats action dispatch, owns the
  terminalWs single-instance reference
- websocket/reject.ts: shared rejectUpgrade helper (replaces five copies)

Service extension:
- NotificationService: setBroadcaster(fn) replaced by subscribe(ws) that
  returns an unsubscriber; broadcastToSubscribers is now internal. Subscriber
  set lives on the service rather than in index.ts.

Wiring in index.ts:
- const app = createApp() already in place from Phase 2
- const { server, wss, pilotTunnelWss } = createServer(app)
- attachUpgrade(server, { wss, pilotTunnelWss })
- app.use('/api/', createRemoteProxyMiddleware())
- /api/system/console-token route now uses mintConsoleSession()
- deploy/down/update routes read the streaming target via getTerminalWs()
  (return type is WebSocket | undefined so the || undefined fallback is gone)

Code review fixes: five duplicated reject helpers collapsed into
websocket/reject.ts; dropped the createTracker/resolveTracker bind
aliases in index.ts so call sites go through the service directly;
removed em dashes; replaced req.url! with req.url || '/'.
2026-04-23 19:31:16 -04:00
Anso ca5a930c68 refactor(backend): extract authMiddleware and introduce createApp factory (phase 2) (#732)
Phase 2 of the index.ts refactor. Pulls the auth middleware and session
cookie issuers into their own module, and introduces the app.ts factory
that owns the first nine steps of the canonical middleware pipeline.

New modules:
- middleware/auth.ts: authMiddleware, issueSessionCookie,
  issueMfaPendingCookie, clearMfaPendingCookie
- app.ts: createApp() factory installing trust proxy, helmet, cors,
  compression, cookieParser, rate limiters, conditionalJsonParser, and
  nodeContextMiddleware. A header comment documents all 16 canonical
  middleware steps and where each currently lives.

Changes:
- middleware/authGate.ts: createAuthGate factory removed; authGate now
  imports authMiddleware directly (the factory existed only to avoid a
  circular import while authMiddleware lived in index.ts).
- services/DatabaseService.ts: added API_TOKEN_SCOPE_TO_ROLE map so the
  auth middleware no longer inlines a stringly-typed record.
- index.ts drops ~260 lines; auth routes, authGate, auditLog,
  apiTokenScope, remaining routes, static serving, and the error handler
  continue to be registered there until their respective phases.
- vitest.config.ts bumps testTimeout to 30s and hookTimeout to 45s so
  fork-pool workers have enough headroom to ts-node-transform the
  growing module graph under CPU contention (64 workers each import the
  full Express stack in beforeAll).

Code review fixes: use getErrorMessage() util in the auth catch block
instead of inline cast; promote the scope-to-role map to a typed
module-level constant.
2026-04-23 19:02:13 -04:00
Anso 856a260a11 refactor(backend): extract rate limiters, body parsing, and request gates (phase 1) (#731)
Phase 1 of the index.ts monolith refactor. Extracts all non-auth middleware
into focused modules. Routes and authMiddleware itself stay in index.ts for
now; Phase 2 introduces createApp() and extracts authMiddleware.

New modules:
- middleware/rateLimiters.ts: globalApiLimiter, pollingLimiter,
  webhookTriggerLimiter, authRateLimiter, ssoRateLimiter, trivyInstallLimiter
  plus the hybrid rateLimitKeyGenerator and isNodeProxyRequest helper
- middleware/jsonParser.ts: conditionalJsonParser that preserves the raw
  stream for remote-proxy forwarding (via helpers/proxyExemptPaths)
- middleware/nodeContext.ts: nodeContextMiddleware
- middleware/apiTokenScope.ts: enforceApiTokenScope + DEPLOY_ALLOWED_PATTERNS
- middleware/authGate.ts: createAuthGate(authMiddleware) factory + auditLog.
  Factory takes authMiddleware as a dependency to avoid a circular import
  until Phase 2 extracts the auth module.
- middleware/errorHandler.ts: central error handler that preserves
  err.status / err.expose from body-parser and other HTTP errors
- helpers/routePatterns.ts: WEBHOOK_TRIGGER_RE shared by rateLimiters and
  authGate

index.ts drops ~290 lines. Middleware registration order is unchanged.
All 1278 tests pass.

Code review fixes: typed ApiTokenScope in apiTokenScope.ts; replaced 2
em dashes with colons (Directive 18); added local CachedProxyFlagReq
type alias for the node_proxy memoization cast; extracted deny() helper.
2026-04-23 18:22:26 -04:00
Anso 929e2fa6b1 refactor(backend): extract types, constants, and guards from index.ts (phase 0) (#730)
* refactor(backend): extract types, constants, and guards from index.ts (phase 0)

Additive, behavior-preserving first step of the modular backend refactor.
Moves purely static artifacts out of backend/src/index.ts so later phases can
extract routes and middleware without touching shared symbols.

New modules:
- types/express.ts: Express Request augmentation
- helpers/constants.ts: PORT, password policy, label colors, cookie names,
  MFA TTLs, hot-path cache TTLs
- helpers/proxyExemptPaths.ts: PROXY_EXEMPT_PREFIXES + isProxyExemptPath
- helpers/cookies.ts: isSecureRequest, getCookieOptions
- helpers/policyGate.ts: buildPolicyGateOptions, runPolicyGate,
  triggerPostDeployScan
- middleware/permissions.ts: ROLE_PERMISSIONS, checkPermission,
  requirePermission
- middleware/tierGates.ts: requirePaid, requireAdmiral, requireAdmin,
  requireNodeProxy, requireScheduledTaskTier + effectiveTier/Variant

index.ts shrinks by ~260 lines; no runtime behavior changes. All 64 vitest
files and 1,278 tests pass.

* refactor(backend): drop unused imports left after phase 0 extraction

LicenseTier, LicenseVariant, DIGEST_CACHE_TTL_MS, and isProxyExemptPath
were imported into index.ts but no longer referenced there after the
phase 0 move; CI lint flagged them as errors.

isProxyExemptPath will be re-imported in phase 1 when the JSON parser
bypass and nodeContext middleware get extracted. Silence the
no-namespace warning on the Express augmentation since the namespace
syntax is required for TypeScript module augmentation.
2026-04-23 17:58:04 -04:00
Anso 1ef96582e1 feat(sidebar): keyboard shortcuts for stack menu actions (#729)
* feat(sidebar): implement keyboard shortcuts for stack menu actions

Shortcut labels shown in the context menu and kebab menu were purely
decorative. This wires them up to the corresponding actions on the
currently selected stack.

Cmd/Ctrl shortcuts: Enter (deploy), . (stop), R (restart), Up (update),
Backspace (delete). Single-key shortcuts: a (alerts), h (auto-heal,
paid), u (check updates), p (pin/unpin).

Guards: shortcuts are blocked when an input element is focused, when the
global command palette dialog is open, when no stack is selected, or
when the stack is busy. All visibility and busy flags from the menu
context are respected.

* docs(sidebar): document keyboard shortcuts for stack actions
2026-04-23 16:45:10 -04:00
Anso d47f6b40e4 fix(login): remove branding duplication, add shimmer and ping dot (#727)
- Remove redundant SENCHO prefix from kicker (already shown in card header)
- Animate the left accent bar with a slow back-and-forth shimmer sweep
- Replace static status dot with animated ping pulse
2026-04-23 10:17:14 -04:00
sencho-quartermaster[bot] 866732f9ba docs: refresh screenshots (#722)
Co-authored-by: AnsoCode <18150933+AnsoCode@users.noreply.github.com>
2026-04-21 12:54:34 +00:00
sencho-quartermaster[bot] 81abd73fc7 chore(main): release 0.63.0 (#700)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com>
2026-04-21 08:52:41 -04:00
Anso 12c2b37510 feat(security): polish scan sheets, fix CVE links, surface policy violations (#721)
* feat(security): polish scan sheets, fix CVE links, surface policy violations

Adds cveUrl helper that rewrites Trivy's 404-ing avd.aquasec.com links to
cve.org for CVE-prefixed IDs (GHSA and misconfig URLs pass through unchanged).
Redesigns both scan sheets with shadow-card-bevel chips, tracked-mono kickers,
severity row tinting with a left accent rail, and tabular-nums timestamps.
Surfaces a destructive policy-violation banner on scans whose policy_evaluation
row flags a block, and fixes the compare sheet's delta ribbon so CRITICAL
net-positive deltas render in destructive (not warning) tone. Backend parses
the JSON policy_evaluation column at the API boundary so the UI receives a
structured object.

* chore(security): suppress CVE-2026-32281 and CVE-2026-32283 in Trivy scan

Both CVEs affect Go stdlib crypto/x509 and TLS in Docker CLI 29.4.0
(Go 1.26.1) and Compose v5.1.2 (Go 1.25.8). No upstream static binary
has been released with the patched Go 1.26.2 or 1.25.9 runtimes yet.

Exposure analysis: the Docker CLI and compose plugin connect to the local
Docker socket (Unix socket, no TLS) and to public registries with well-known
CAs. Neither CVE is exploitable in this configuration. Added alongside
sibling entries already in .trivyignore for the same binary versions.

Revisit on next Docker CLI and Compose upstream release.
2026-04-21 08:51:35 -04:00
Anso e4fdb1cd6c fix(security): convert scan history from full page to sheet overlay (#720)
Scan history is now a right-side sheet that layers over the current view
(typically Resources Hub) instead of a full-page activeView branch. The
sheet opens via the existing navigation event, fetches only when open,
dismisses on Escape or overlay click, and preserves the nested scan-details
and scan-compare sheets intact via Radix portal stacking.

The fetch effect now resets selection and page state on active-node change
exactly once, avoiding a double-fetch on node switches.
2026-04-21 08:01:31 -04:00
Anso 661b9c638b feat(security): enforce scan policies as a pre-deploy gate (#719)
Policies with block_on_deploy=1 now scan every stack image before
docker compose up runs and reject the deploy with HTTP 409 on violation.
The UI opens a dialog listing offending images; admins can override per
deploy with ?ignorePolicy=true, and every bypass is recorded in the
audit log with the originating route, actor, policy, and image list.

When Trivy is not installed on the target node the gate fails open with
a warning notification, so teams are never locked out by tooling state.
Post-deploy and scheduled scans still evaluate matching policies and
dispatch warnings on violations to surface drift on long-running stacks.

Public API additions: policy and suppression CRUD under /api/security,
plus the documented 409 block-response shape on all deploy paths.
2026-04-21 00:14:11 -04:00
Anso aa10db1d09 fix(trivy): remove unsupported --no-progress flag from trivy config (#718)
The `trivy config` subcommand does not accept `--no-progress`; the flag
exists only on `trivy image`. Every stack configuration scan therefore
failed with `FATAL Fatal error unknown flag: --no-progress`, and the
"Scan configuration" action on the stack details page has been
non-functional since it shipped.

Removing the flag is the complete fix. `trivy config` is silent by
default, so the flag was redundant even if it had been accepted.

A new Vitest spec pins the exact argument vector
(`['config', '--format', 'json', '--quiet', <stackPath>]`) so a future
edit cannot reintroduce the bug, and exercises the success path end to
end: status transitions to `completed`, misconfig severities tally
correctly, and `highest_severity` rolls up to the worst finding.
2026-04-20 23:14:23 -04:00
Anso 3e1fb76bd0 feat(notifications): add per-node filter and 60s refetch safety net (#717)
Two polish improvements to the aggregated notifications inbox:

- Per-node filter dropdown in the bell panel (hidden on single-node
  installs) so fleet operators can triage events from a specific box.
  Selected node falls back to "All nodes" automatically if that node is
  removed from the registry.
- 60-second safety-net poll that reconciles the list so events missed
  during a WebSocket reconnect backoff appear without a manual refresh.
  Uses a ref indirection to pin the interval to the latest
  fetchNotifications closure.
2026-04-20 21:37:34 -04:00
Anso 08f57c7141 feat(settings): surface security, notifications, and app store on remote nodes (#716)
Flip Security (Trivy), Notifications (agents + history), and App Store from
global-and-hidden-on-remote to node-scoped so operators can manage them when a
remote node is selected in the node picker. The primary instance proxies the
calls to each remote, which resolves the correct per-instance binary state,
agent config, and template registry.

Backend: key `agents` and `notification_history` by `node_id` with idempotent
column-add migrations and a `(node_id, type)` unique index on agents, matching
the Labels pattern. Thread `req.nodeId` through the /api/agents and
/api/notifications routes. Internal NotificationService and ImageUpdateService
writes resolve the middleware default via `NodeRegistry.getDefaultNodeId()` so
monitor-emitted rows share a bucket with user-facing ones (avoids split-brain
where the UI sees test notifications but not internal alerts).

Frontend: split Security on remote to render only the scanner card and hide
scan policies and CVE suppressions (those remain control-plane-only). Drop the
misleading "Always Local" badge on Developer since retention windows govern
backend jobs, not UI state. Flip the App Store registry to node-scoped.

Docs: add a "What Settings apply per node" table to multi-node, clarify
remote alert setup in alerts-notifications, and note Trivy's per-host install
in vulnerability-scanning.
2026-04-20 21:04:09 -04:00
Anso a42cc5bf03 fix(ui): raise toast z-index above dialog overlays (#715) 2026-04-20 18:23:38 -04:00
Anso 0a0198013d feat(auth): redesign login, MFA, and setup surfaces with cockpit voice (#714)
* feat(auth): redesign login, MFA, and setup surfaces with cockpit voice

Applies the cockpit design system to every auth surface: Login, MFA
challenge, first-boot Setup, and the three MFA dialogs (Enroll, Backup
Codes, Disable). Introduces shared primitives under components/auth/:
AuthCanvas shell with bevelled card and cyan left rail, AuthStepHeader
for tracked-mono kicker plus italic hero pair, OtpDigitField with six
recessed digit cells and auto-submit, and ErrorRail for consistent
inline errors.

Preserves all behaviour: Local/LDAP toggle, dynamic SSO providers,
auto-submit TOTP, backup-code fallback with dash formatting, rate-limit
countdown, three-step enrollment, cold-start setup. Surface-only change;
AuthContext, routing, and endpoints untouched.

* test(e2e): update MFA spec selectors to match redesigned auth surfaces

The auth redesign renamed buttons, restyled the backup-mode toggle to
bracketed mono, changed input ids on the challenge + disable dialogs,
and made the challenge TOTP path auto-submit (no explicit Verify
button). Updates the spec accordingly:

- Enroll step 1 button: Next -> Continue
- Enroll step 3 button: "saved these" -> Done
- Challenge heading: "Two-factor authentication" -> "Verify"
- Backup toggle: "Use a backup code instead" -> "Use backup code"
- Challenge verify button: "Verify and sign in" -> "Verify"
- Challenge input id: #mfa-code -> #mfa-otp (TOTP) / #mfa-backup (backup)
- Disable dialog backup input id: #mfa-disable-code -> #mfa-disable-backup

All six MFA tests pass locally. No production code changed.
2026-04-20 17:58:57 -04:00
Anso d95e154aeb feat(fleet): interactive topology with ReactFlow hub-and-spoke layout (#713)
Replace the static SVG fleet topology with a ReactFlow canvas laid out
via dagre. Each node renders as a rack card with status pill, type
badge, CPU/MEM/DISK bars, and stack/running counts. Pan, zoom, drag,
and minimap are enabled; user-dragged positions persist across the
30-second poll so live metric updates no longer reset layout.
2026-04-20 16:16:02 -04:00
Anso 6f132b7ffe feat(fleet): reorganize overview page for clarity and density (#712)
* feat(fleet): reorganize overview page for clarity and density

Scope Grid/Topology to the Overview tab (moved from above the tab bar so it
no longer implies it applies to Snapshots). Move Check Updates and Refresh
onto the tab bar row, right-aligned.

Compact the overview toolbar: constrain the sort combobox to a fixed width so
it no longer stretches full-page, and collapse the Status, Type, Severity,
and Tags pill groups into a single Filters popover with an active-count
badge and an inline Clear all filters action.

Render the local node card in the same responsive grid as remote nodes
instead of a dedicated full-width row. Visual distinction is preserved
through the existing brand gradient, cyan rail, ring, and "Local" badge.

* fix(fleet): stop local card from stretching when remote expands

Merging the local and remote node cards into a single responsive grid
meant CSS grid's default align-items: stretch made every cell in a row
match the tallest one. Expanding stack details on a remote card pulled
the local card up with it.

Add items-start on the merged grid so each cell sizes to its own
content.
2026-04-20 14:53:12 -04:00
Anso 856de35a52 feat(search): add global Ctrl+K command palette (#711)
* feat(search): add global command palette with cross-node stacks

Press Ctrl+K from anywhere to open a search palette that jumps to
pages, switches nodes, or opens stacks on any online node in the
fleet. Trigger lives as an icon in the top bar. Replaces the former
sidebar-scoped Ctrl+K handler.

The cross-node stack search fan-out is extracted into a shared hook
so the sidebar and palette stay in sync on the same debounce +
abort shape.

* fix(search): drop render-time ref write and effect-based query reset

Replace `nodesRef.current = nodes` during render with direct use of
`nodes` in the stack select callback, and fold the query-reset logic
into a single `onOpenChange` handler so it no longer runs via an
effect. Both changes resolve react-hooks rule violations that broke
frontend lint in CI.
2026-04-20 13:57:40 -04:00
Anso a41af47ff3 feat(fleet): aggregate labels across nodes and allow remote edits (#710)
Labels settings are now visible on remote nodes (scope: node, no longer
hidden on remote context). The LabelsSection already routes through the
active-node proxy, so edits land on whichever node the operator is viewing.

Fleet overview's label filter previously fetched only the control-plane
node's labels and assignments, so remote stacks could never match the
filter. Rewrote aggregation to fan out /labels and /labels/assignments
to every online node via fetchForNode + Promise.allSettled with a 5s
timeout per request. The palette dedupes by (name, color) so identical
labels on multiple nodes collapse into one entry while same-name +
different-color stay distinct. The assignment map is nested by nodeId
to avoid cross-node stack-name collisions.

Keyed the label refetch effect on a stable online-node id signature
rather than the nodes array reference, so the existing 30s overview
poll (and 5s fast-poll during updates) does not cascade into repeated
fleet-wide label fetches.
2026-04-20 12:56:00 -04:00
Anso 2b499cb2c9 feat(design-system): retune oklch tokens to cozy-pebble palette (#709)
Apply design audit section 13's proposed chroma/lightness values to
frontend/src/index.css. Dark-theme accents now match the audit swatches
exactly: brand 0.78 0.11 195, success 0.78 0.16 155, warning 0.82 0.14
75, destructive 0.72 0.18 20. Surface hierarchy shifts to a warm pebble
cast (hue 80, chroma 0.003 to 0.01); card anchors to the audit's
0.14 0.005 80 surface swatch; popover bumps to 0.16 to preserve the 4
percent lightness gap against card. Light theme mirrors symmetrically.

NetworkTopologyView keeps the react-flow inline-style escape hatch but
hoists the brand literal into a BRAND_COLOR constant so MiniMap and
EDGE_COLORS share one source of truth.

Also normalize em-dash comment separators to colons across index.css
and drop a duplicated dark-background literal in favor of
var(--background).
2026-04-20 12:19:25 -04:00
Anso c7dfde4b79 fix(sidebar): distinct fuchsia notification dot for update indicator (#708)
The pulsing dot signaling an available image update used --info (blue
hue 250), the same hue as --label-blue, so the two blended visually
when a stack carried a blue label. Introduce a dedicated --update
token in vibrant fuchsia (hue 320) sitting outside the label palette,
and render both the sidebar StackRow and the fleet NodeManager
indicator as a notification-dot pattern (solid stationary core with
animate-ping halo). Aligns with the design rule that static state
should not pulse.
2026-04-20 11:24:41 -04:00
Anso af59836538 chore(sidebar): document labels menu invariants and remove dead code (#707)
Document the hidden constraints that PR #706 left in the codebase so they
are not accidentally removed in a future cleanup pass:

- DropdownMenuSubContent and ContextMenuSubContent must stay Portal-wrapped
  so sub-menus escape ancestors with overflow-x-hidden.
- refreshLabels must stay stable via useCallback because it is captured by
  buildMenuCtx's memoization and passed as a prop.

Also delete LabelAssignPopover.tsx, which had zero consumers after the
create-and-assign flow moved into the menu-layer inline form.
2026-04-20 10:34:45 -04:00
Anso 75370d8fce feat(sidebar): inline label create, live sync, and kebab submenu parity (#706)
Portal-wrap DropdownMenuSubContent so the kebab Labels submenu renders outside
the clipped dropdown container, matching ContextMenuSubContent and fixing the
empty/broken kebab submenu.

Thread onLabelsChanged from LabelsSection through SettingsModal to
EditorLayout so label creates and deletes in Settings propagate to the
sidebar menus without a page refresh.

Add an inline "New label" form in both the kebab and context menu label
submenus that creates and assigns a label in one interaction, removing the
Settings round-trip from the label assignment flow.
2026-04-20 09:57:20 -04:00
Anso 5f2d67848c fix(dashboard): remove peak indicator dot from CPU sparkline (#705)
The dot on the CPU sparkline added visual noise without adding
information beyond what the textual "peak X% @ HH:MM" label
already conveys. Disable showPeak on the CPU Sparkline; the peak
text label is preserved.
2026-04-20 08:34:45 -04:00
Anso 9f861e0072 fix(app-store): use stack name as compose service key (#704)
* fix(app-store): use stack name as compose service key

App Store deployments hardcoded the compose service to "app", so every
container's com.docker.compose.service label collapsed to "app". Global
logs and AutoHeal policies key off that label, making it impossible to
distinguish between deployed apps. Pass the already-validated stack
name through to generateComposeFromTemplate so each app gets a
descriptive service identifier.

* test(template-service): use {2} quantifier to satisfy no-regex-spaces

ESLint's no-regex-spaces rule flagged two consecutive spaces in the
regex literal as an error. Swap for the {2} quantifier to keep the
assertion identical while clearing the lint gate.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 07:57:16 -04:00
Anso 370b67d7ec feat(sidebar): cockpit redesign with grouped stacks and activity footer (#702)
* chore: ignore .superpowers/ brainstorm scratch dir

* feat(sidebar): add useStackMenuItems hook with grouped menu model

Pure transform hook that converts StackMenuCtx into four ordered MenuGroup
arrays (inspect, organize, lifecycle, destructive). Shared type contract in
sidebar-types.ts gives both the ContextMenu and DropdownMenu a single source
of truth so they cannot drift. Covered by 8 unit tests.

* refactor(sidebar): stabilize useStackMenuItems memoization deps

Destructure menuVisibility flags into primitive deps so inline object
literals from callers do not defeat memoization. Add a test confirming
isBusy disables all lifecycle items.

* feat(sidebar): add usePinnedStacks hook with per-node localStorage

* refactor(sidebar): stabilize usePinnedStacks eviction signal and isPinned dep

Change evictedOldest shape to { file, seq } so consumer effects re-fire on
repeated evictions. Narrow isPinned's useCallback dep to the current node's
pinned list so it only rebinds on local changes. Add test for eviction
side-effect and a second test for the seq counter.

* feat(sidebar): add useSidebarGroupCollapse hook with per-node keys

* refactor(sidebar): tighten useSidebarGroupCollapse effect ordering

Collapse the two write/read effects into a single skip-next-write ref
pattern so switching nodes no longer writes the previous node's map under
the new key before hydration. Also skip the no-op mount write. Add test
for setCollapsed.

* feat(sidebar): add row + group-header style helpers

* feat(sidebar): add SidebarBrand with mono kicker + serif hero

* feat(sidebar): add SidebarActions wrapper for create + scan

* feat(sidebar): extract SidebarSearch with kbd pill

* feat(sidebar): add StackRow with cyan-rail active state

* refactor(sidebar): dedupe tooltip markup in StackRow, widen test coverage

Extract a local RowTooltip helper so the update and git-pending branches
share the CursorProvider scaffolding. Add four behavioral tests covering
click, keyboard activation, kebab stop-propagation, and the busy loader
branch.

* feat(sidebar): unify context + kebab menus via useStackMenuItems

* feat(sidebar): add StackGroup with collapse and pinned variant

* feat(sidebar): add StackList with pinned + label groups

* feat(sidebar): add SidebarActivityTicker with idle fallback

* feat(sidebar): add StackSidebar container composing the regions

* feat(sidebar): replace sidebar block with StackSidebar composition

* docs(sidebar): add stack sidebar feature page with screenshots

* fix(sidebar): satisfy react-hooks purity and memoization rules

* fix(sidebar): restore "Sencho Logo" alt text for E2E selector
2026-04-19 21:45:01 -04:00
Anso 490c89c049 feat(ui): redesign host console as cockpit surface (#701)
* feat(ui): redesign host console as cockpit surface

Rework the Console view into the cockpit language introduced in #699.
The page is now a PageMasthead strip plus a terminal well plus a
floating chip strip.

The masthead shows connection state (`Connected`, `Reconnecting`,
`Disconnected`) with a cyan rail, a pulsing status dot while the
session is live, an italic state word, and a tracked-mono kicker of
`HOST CONSOLE · {node}`. The right side surfaces three metadata tiles:
shell, current viewport dimensions, and session uptime. When the
console was opened from a stack, a small back link sits beside the
state word so operators can return to the stack editor without
leaving the cockpit.

The vestigial Close Console button in the header is gone. The
floating chip strip replaces it with four controls that are more
useful in context: Copy (current selection), Clear (scrollback),
Download (full scrollback via SerializeAddon), and Reconnect (close
and reopen the WebSocket in place).

* docs(host-console): refresh screenshots for cockpit redesign

Adds masthead and chip-strip close-up screenshots to the Host Console doc
and refreshes the overview screenshot to match the new cockpit surface.
2026-04-19 17:14:25 -04:00
Anso b95442c8f7 feat(ui): redesign global logs as cockpit surface (#699)
Rework the Logs view into the cockpit language: a PageMasthead strip with
cyan rail, pulsing live dot, italic state word, and tracked-mono kicker;
a SignalRail of EVENTS/MIN (with rolling 60s sparkline), ERRORS, WARNINGS,
and CONTAINERS tiles; a segmented filter strip; a day-banded feed with
severity dots and whole-row tint on ERROR/WARN; and a floating glass chip
strip for pause/resume, clear, and download.

Decouple the live log stream from the Developer Mode toggle so SSE runs by
default for everyone, with polling as an invisible fallback when
EventSource is unavailable. Drop the Standard Log Polling Rate setting
and the global_logs_refresh key it persisted; Developer Mode still gates
Real-Time Metrics and Debug Diagnostics and nothing else.

Introduces the shared PageMasthead and SignalRail primitives for reuse
across other cockpit pages.
2026-04-19 16:25:54 -04:00
Anso ef4455f68d refactor(ui): rework top bar nav as cockpit switch row (#697)
Replace the sliding accent pill and blurred underline with material-at-
rest tabs that speak the cyan identity language used elsewhere in the
chrome. Each tab is a full-height button with a tracked-mono uppercase
label, and the active tab carries a crisp 2px cyan rail flush with the
bar's bottom edge.

Drops the <Highlight> primitive, the springs import, and the unused
navTabValue memo/prop from EditorLayout.
2026-04-19 15:11:16 -04:00
sencho-quartermaster[bot] 9483a09cee docs: refresh screenshots (#698)
Co-authored-by: AnsoCode <18150933+AnsoCode@users.noreply.github.com>
2026-04-19 19:06:38 +00:00
sencho-quartermaster[bot] 75d93d20db chore(main): release 0.62.0 (#686)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com>
2026-04-19 15:04:43 -04:00