mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 17:08:10 +00:00
801a098a5b550dbc8089117b288a0834fcfe6e95
193 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
801a098a5b |
feat(files): per-stack file explorer (#780)
* feat(files): backend foundation for stack file explorer
Install multer for multipart file upload handling. Add
isValidRelativeStackPath to validation.ts to guard client-supplied
relative paths against traversal, absolute paths, NUL bytes, backslash
injection, and double-slash segments. Add isBinaryBuffer to a new
binaryDetect.ts utility for heuristic text/binary detection via
NUL-byte fast exit and non-printable byte ratio sampling.
* fix(files): reject bare dot segments in isValidRelativeStackPath
* feat(files): add safe stack-scoped file I/O methods to FileSystemService
Adds FileEntry interface and seven new public methods to FileSystemService
for stack-scoped file operations: listStackDirectory, readStackFile,
streamStackFile, writeStackFile, deleteStackPath, mkdirStackPath, and
statStackEntry.
Each method routes through a private resolveSafeStackPath helper that
enforces two-phase path containment: a pre-realpath lexical check plus a
post-realpath symlink-escape check. ENOENT targets are handled by walking
up to the deepest existing ancestor, realpaths that ancestor, and
reattaching the remaining suffix.
Binary detection delegates to isBinaryBuffer; path safety delegates to
isPathWithinBase. Protected file names and the MIME map are module-level
constants to avoid repeated allocation.
* feat(files): frontend API wrappers and Monaco language helper
* fix(files): tighten stackFilesApi error handling and localOnly support
* fix(files): FileSystemService safety and correctness fixes
* feat(files): add file explorer API endpoints to stacks router
* feat(files): FileTree and FileTreeNode components
* fix(files): route security hardening and stream cleanup
* fix(files): FileTree accessibility, icon stroke, stale fetch guard
Add strokeWidth={1.5} to all Lucide icons in FileTreeNode to match the
design system. Add aria-expanded to directory rows for accessibility.
Guard handleDirClick .then() callbacks against stale stack name
references when the component re-renders with a new stack. Add
toast.info fallbacks when compose.yaml or .env is clicked without a
navigation callback registered.
* feat(files): FileViewer, FileUploadDropzone, NewFolderDialog, DeleteFileConfirm
* fix(files): resolve code quality findings in file explorer components
- Move editorOptions useMemo above conditional returns in FileViewer (Rules of Hooks fix)
- Fix blob download: append anchor to DOM before click, defer URL revoke 100ms
- Keep protected-file confirm input visible during NOT_EMPTY recursive retry in DeleteFileConfirm
- Remove non-functional cursor-pointer/onClick from Community upgrade pill in FileUploadDropzone
- Add success toast on folder creation in NewFolderDialog
- Switch all (e as Error).message casts to instanceof Error narrowing
* test(files): unit tests for binary detection, stack path safety, and file explorer routes
- binary-detection.test.ts: covers isBinaryBuffer edge cases (empty, NUL,
PNG header, threshold boundary, sampleBytes parameter)
- filesystem-stack-paths.test.ts: covers isValidRelativeStackPath (accepts/
rejects matrix) and FileSystemService stack methods against a real temp dir
(listStackDirectory sort and protection flags, readStackFile text/binary/
oversized paths, writeStackFile/Buffer, deleteStackPath, mkdirStackPath,
traversal guard); platform-specific empty-dir/NOT_EMPTY cases skip on Windows
- stack-files-routes.test.ts: route-level integration tests for all seven
file explorer endpoints; covers auth gating, Community-tier 403 gates,
input validation, 413 TOO_LARGE upload limit, and 204/200 happy paths
* feat(files): StackFileExplorer container with lazy tree, viewer, and action bar
* fix(files): add Download button to explorer toolbar, fix Community upgrade pill, reset state on stack change
* test(files): add missing test coverage for file explorer routes and service
* feat(files): add Files tab to EditorLayout with StackFileExplorer integration
* fix(files): add defensive activeTab guard to saveFile and discardChanges
* test(files): unit tests for FileTree expand/collapse and FileViewer render modes
Covers the three FileViewer content modes (text/Monaco, binary panel,
oversized panel) and the FileTree expand/collapse/cache cycle: first
expand fetches the subdirectory, second click collapses without a fetch,
third click re-expands from the in-memory cache without a second fetch.
* test(e2e): file explorer community and skipper+ flows
Covers the full file-explorer feature surface in two describe blocks:
Community (read-only): intercepts /api/license to simulate community
tier, confirms the upgrade pill is visible in the left pane, and
asserts that the Save button is absent after opening a text file.
Skipper+ (full CRUD): uploads a text file and confirms it appears in
the tree; edits config/app.conf and saves via Monaco; deletes an
uploaded file and asserts the tree entry is gone; issues a raw HTTP
request to the download endpoint and checks for status 200 and the
content-disposition: attachment header.
Also adds data-testid="file-action-delete" to the action bar Delete
button in StackFileExplorer for stable targeting, and exports
waitForStacksLoaded from e2e/helpers.ts to eliminate the three
identical local copies in stacks, deploy-log-panel, and stack-files
spec files.
* fix(e2e): improve test isolation and selector stability in stack-files spec
Move beforeEach seed to beforeAll/afterAll so fixtures are created once per
suite, not before every test. Extract shared seedSuite/teardownSuite helpers
to eliminate the duplicate beforeAll/afterAll blocks. Wrap teardown in
try/catch so failures log a warning rather than masking test results.
Replace waitForTimeout(500) with a deterministic expect on the file tree
sentinel. Add data-testid="anatomy-files-btn" and data-testid="delete-confirm-btn"
to replace the fragile button text/positional selectors. Assert Save button
starts disabled before editing.
* docs(files): add stack file explorer documentation
Add user-facing guide for the stack file explorer feature covering
tier access (Community read-only, Skipper+ read-write), viewing
limits, upload/download caps, protected file routing, and
troubleshooting. Update the editor page to reference the new guide
and register the page in the navigation.
* fix(docs): use canonical Skipper tier name in file explorer overview card
* fix(files): resolve lint errors blocking CI
Remove unnecessary backslash escape before double-quote in the
Content-Disposition regex (no-useless-escape). Replace five synchronous
setState resets at the top of the FileTree mount effect with a React key
prop on the FileTree element in StackFileExplorer so remounting resets
state automatically, eliminating the react-hooks/set-state-in-effect
violation.
* test(files): fix e2e seeding to work on community-tier CI
Replace the browser-side paid upload/mkdir API calls in seedTestStack with
direct Node fs writes. The upload and folder endpoints require Skipper+ so
they returned 403 on CI, which runs with no license set. Stack creation
via POST /api/stacks stays as an API call since it is community-allowed and
keeps the backend registry in sync.
Add a per-test tier check in the Skipper+ beforeEach that skips gracefully
when the instance is community, matching the pattern in auto-heal-policies.
|
||
|
|
abee078741 |
feat(scheduler): add auto_backup, auto_stop, auto_down, auto_start and delete_after_run one-shot mode (#777)
* feat(scheduler): add auto_backup, auto_stop, auto_down, auto_start actions and delete_after_run one-shot mode Extends the scheduler with four new stack-targeted actions: - auto_backup: backs up stack compose files and .env using the existing FileSystemService.backupStackFiles primitive - auto_stop: runs compose stop (containers preserved) - auto_down: runs compose down (containers removed) - auto_start: runs compose up -d via deployStack (universal start for both stopped and down stacks) Adds delete_after_run boolean column to scheduled_tasks. When enabled, the task self-deletes after its first successful execution; failures keep the task so the user can debug and retry. All four new actions gate at Admiral tier, consistent with restart/snapshot/prune. Migration is idempotent (maybeAddCol). * docs(scheduler): update scheduled-operations doc with new lifecycle actions and delete-after-run Adds the four new actions (Backup Stack Files, Stop Stack, Take Stack Down, Start Stack) to the action table. Documents the delete-after-run one-shot mode with its success-only deletion semantics. Adds the Stack Lifecycle Scheduling section explaining stop-vs-down semantics and the local-execution boundary. Adds three troubleshooting entries: auto-start on a missing compose folder, auto-backup single-slot overwrite by design, and one-shot task disappearing after successful run. Updates the timeline description from four to five lanes. Refreshes screenshots to show the new dialog layout with the Lifecycle lane visible. |
||
|
|
e0034132b4 |
feat(notifications): match routing rules by labels and categories (#776)
- Add label_ids and categories columns to notification_routes via idempotent migration - Matcher logic always evaluates routes (AND semantics across all non-empty matchers) - getStackLabelIds skips DB call when no enabled route uses label filtering - Extract ALL_NOTIFICATION_CATEGORIES array from NotificationService as single source of truth - Derive VALID_CATEGORIES set from the array in the route handler - Extract validateLabelIds and validateCategories helpers to remove POST/PUT duplication - Extract tryAddColumn as a private DatabaseService class method (removes 5 local re-declarations) - Extract CATEGORY_LABELS to frontend/src/lib/notificationCategories.ts (shared by NotificationPanel and NotificationRoutingSection) - Frontend form adds label and category multiselects with AND-filter hint - Route cards show label and category badges; empty-matcher routes show 'Matches all alerts' - Add tests for category-only, label-only, and combined AND-semantics routing |
||
|
|
fcbdd59ec2 |
fix(notifications): scope routing rules to nodes via node_id column (#775)
Adds a nullable node_id column to notification_routes (null = any node, integer = fire only when the alert originates from that specific node). This fixes a multi-node fleet defect where a route scoped to "my-app" would fire on every node that hosts a stack with that name. Backend changes: - DatabaseService: idempotent migration adds node_id INTEGER NULL and a composite index on (node_id, enabled, priority); the two statements are in separate try-catch blocks so the index is always created even when the column was added in an earlier run - NotificationService: route matcher now pre-filters by node_id before checking stack_patterns (== null matches any node) - notifications route: POST/PUT accept optional node_id, validated to be null or the local node's ID; NodeRegistry guards against cross-node misroutes Frontend changes: - NotificationRoutingSection: node scope Select field uses useNodes() from NodeContext (no extra API call) to populate the local node option - Route cards show a node badge when node_id is set Tests: 3 new tests covering node-match, node-mismatch, and null-scope; all 75 files (1413 tests) passing. |
||
|
|
44dba59cab |
feat(notifications): add structured category enum to dispatcher and history (#774)
Introduce a NotificationCategory string-literal union (11 values) and thread it through dispatchAlert as a required second argument. All callers (DockerEventService, AutoHealService, ImageUpdateService, MonitorService, PolicyEnforcement, policyGate, SchedulerService, imageUpdates route) pass an explicit category at every call site, giving TypeScript compile-time enforcement that no new emit site can be added without choosing a category. DatabaseService gains an idempotent migration that adds a nullable category TEXT column to notification_history; existing rows keep category=NULL (displayed as Uncategorized in the UI). The getNotificationHistory method accepts an optional category filter that is forwarded from the GET /api/notifications/history route via a ?category= query param. NotificationPanel gains a category Select dropdown so users can filter history by category. The frontend types mirror the backend union so API responses are type-safe end-to-end. All 75 test files (1410 tests) updated to the new 4-arg dispatchAlert signature and passing. |
||
|
|
a74564fd61 |
feat(scheduler): support fleet-wide auto-update schedules per node (#773)
Allow a scheduled task with action='update' and target_type='fleet' to update every eligible stack on a node in a single schedule entry. The executor respects each stack's per-stack auto-update policy via a single batch query, skipping stacks that have opted out. For remote nodes the request proxies to the remote Sencho instance, which already enforces the same policy in its /api/auto-update/execute endpoint. Backend route validation now accepts update+fleet as a valid combo (previously only update+stack was allowed) and requires node_id. Frontend adds an "Auto-update All Stacks" option to the scheduled-task creation form with a node selector and descriptive helper text. |
||
|
|
af9cb0aa63 |
feat(auto-update): per-stack auto-update enable/disable toggle (#771)
* feat(auto-update): add per-stack auto-update enable/disable toggle Paid users (Skipper and Admiral) can now opt individual stacks out of scheduled auto-updates from the stack context menu without disabling the global feature. - Add stack_auto_update_settings table (node_id, stack_name) with default enabled=true; four typed DatabaseService accessors with parameterized queries. - Add GET /stacks/auto-update-settings, GET /stacks/:name/auto-update, and PUT /stacks/:name/auto-update (requirePaid + requireAdmin). PUT broadcasts state-invalidate with action auto-update-settings-changed so all open tabs refresh immediately. - Stack DELETE clears the auto-update setting row alongside stack_update_status. - autoUpdateRouter /execute skips disabled stacks before any registry call; skip is recorded in the results array. Manual Update actions are not affected. - Add Auto-update: Enabled/Disabled toggle in the stack inspect group (paid tiers only, hidden for Community, consistent with Auto-Heal). Toggle uses optimistic update with revert-on-error toast. - AutoUpdateReadinessView shows an Auto: Off pill and disables the Apply now button for stacks with auto-updates off. Detection still runs so the readiness card remains visible. - Add 21 backend Vitest tests covering DB round-trips, endpoint auth and tier gates, execute skip for both wildcard and named targets. Add 3 frontend hook tests for toggle visibility and callback behavior. * docs(auto-update): document per-stack auto-update control Add a Per-stack control section to the auto-update readiness page explaining how to disable and re-enable auto-updates for individual stacks, what disabling means (scheduled apply skipped; detection still runs; manual update unaffected), and a troubleshooting entry for scheduled runs not applying to a specific stack. |
||
|
|
5c5021846a |
feat(events): broadcast state-invalidate on docker events so dashboard updates live (#768)
Dashboard and sidebar status indicators previously only refreshed on a
5-30 second polling cadence: a container restart, a degraded -> healthy
transition, or a stack update was invisible until the next tick.
Add a lightweight, non-persisted "state-invalidate" envelope on the
existing /ws/notifications WebSocket:
Backend
- NotificationService.broadcastEvent: sibling of dispatchAlert that
pushes an arbitrary {type, ...} envelope to every subscriber WITHOUT
writing to the alerts history (these are pure ephemeral signals).
- DockerEventService.handleEvent: emit the envelope for state-changing
container actions (start/die/kill/destroy/create/restart/pause/
unpause/health_status/rename/update). Carries node id, stack name
(from the compose project label), container id, action, and
timestamp.
Frontend
- EditorLayout's two notification WebSocket handlers (local plus
per-remote-node) branch on type. On state-invalidate they re-emit a
window CustomEvent and trigger a debounced (250ms) refreshStacks so
a burst of events from compose recreating multiple services
collapses to one refetch. The refresh callback is held in a ref so
the long-lived WS effect never closes over a stale function.
- useDashboardData listens for the same window event and refetches
/stats, /system/stats, and /stacks/statuses on every signal.
Historical metrics stay on their 60s polling cadence (10-minute
trend data, not a live indicator).
Tests
- Three new docker-event-service cases assert broadcastEvent fires on
start and health_status events with the correct envelope shape, and
does not fire on non-state actions like exec_create.
- Existing 28 cases updated with the broadcastEvent mock so the
subscriber stub matches the new shape.
Polling stays as a safety net at the same intervals; the WS path is
the fast path. Multi-node fleets benefit on the local node today;
extending the remote forwarder to relay state-invalidate is a
recommended follow-up.
|
||
|
|
584cda7182 |
fix(auto-update): label same-tag rebuilds as 'Rebuild available' instead of '10.11 -> 10.11' (#766)
When a registry pushes a new build of an image at the same tag (digest changes, tag does not), the preview service set next_tag to the same string as current_tag and the readiness view rendered '10.11 -> 10.11', which reads as a UI bug. Add an update_kind field to UpdatePreviewSummary that distinguishes: - 'tag' - at least one image has a strictly newer tag - 'digest' - the only updates available are same-tag rebuilds - 'none' - nothing to apply The frontend now branches on update_kind and renders 'Rebuild available' next to the current tag for the digest case, leaving the version-arrow diff for genuine tag bumps. Three new buildSummary cases lock in the kind classification. |
||
|
|
9e0f521ea8 |
fix(monitor): include node name in janitor alert and stop firing on near-empty hosts (#765)
The Docker janitor watchdog had three problems on multi-node fleets: 1. The alert text said "Your system has accumulated X GB" with no node identifier, so on a fleet view the operator could not tell which node was complaining. Resolve the local node via NodeRegistry and put the node name in the message. 2. The threshold gate was a single comparison against the user's configured GB value. A small or accidentally tiny threshold made the alert fire on hosts with effectively no waste. Add a 100 MB absolute floor so trivial cruft never triggers a notification. 3. The unit parser only matched uppercase "GB|MB|KB|B" and dropped "TB" entirely. Modern Docker emits "kB" with a lowercase k, which silently contributed zero bytes to the running total. Normalise the unit to uppercase before the comparison and add the TB case. |
||
|
|
24c0a2833b |
fix(security): clear cached policy evaluations when a scan policy is deleted (#758)
Vulnerability scans cache their policy verdict as a JSON blob in vulnerability_scans.policy_evaluation. Deleting a scan policy used to remove only the policies row and leave those blobs intact, so the scheduler kept emitting violations and stacks remained marked as blocked against a policy that no longer existed. deleteScanPolicy now nulls out policy_evaluation on every scan whose JSON references the deleted policy id, then deletes the policy row, in one transaction. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 || '/'.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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>
|
||
|
|
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. |
||
|
|
ed2a16af79 |
feat(notifications): deep-link bell rows to source stack and container logs (#692)
Add stack_name and container_name columns to notification_history so bell rows can act as jump points. Producers (AutoHeal, Docker events) pass the container context through dispatchAlert; the panel renders routable rows as buttons that load the target stack and, when a container name is present, open its logs modal. Non-structural notifications stay as passive display rows. |
||
|
|
a65a1c0e86 |
feat(stack-view): per-container health strip and structured logs viewer (#689)
* feat(stack-view): per-container health strip and structured logs viewer Replaces the flat container list with a per-container health strip showing healthcheck state, uptime, port mapping with an open-app link, and live cpu/memory/network sparklines fed by a 60-sample ring buffer on the stats WebSocket. Adds a structured logs viewer that parses docker timestamps (emitted by the -t flag on the logs stream) and classifies each line by level. Rows render as a DOM grid with filter pills (all / info / warn / err with count), following indicator, and plain-text download. A segmented toggle switches between the structured viewer and the original xterm view; the choice is persisted in localStorage. * fix(stack-view): disable no-control-regex for ANSI escape pattern ANSI escape sequences start with ESC (0x1B), which is a control character. The regex is intentional and cannot be rewritten without it. |
||
|
|
82aabfe64c |
feat(stack-view): identity header with health state and action hierarchy (#688)
* feat(stack-view): identity header with health state and action hierarchy Redesign the stack view header around three questions: what is this, is it healthy, what does it do. Surface Docker healthcheck state, primary image tag, and image digest; group actions by frequency. - Backend: extend /api/stacks/:name/containers with healthStatus (healthy / unhealthy / starting / none), Image, and ImageID by inspecting each container in parallel. - Frontend: replace flat CardHeader with breadcrumb, italic serif title, colored state pill with pulse, and a mono image/digest line with a one-click copy button for the full digest. - Frontend: action hierarchy - primary cyan Restart/Start, outline Stop and Update, and an overflow menu for Rollback, Scan config, and Delete. - Docs: new Stack header section and updated controlling-a-running-stack tables showing primary/secondary/overflow grouping. * test(e2e): open overflow menu to reach stack Delete action Destructive actions now live under the stack toolbar overflow menu rather than as a flat top-level button, so the delete flow must click More actions before selecting the Delete menu item. |
||
|
|
591dc75d1e |
feat(audit-log): signal rail, day-banded stream, anomaly detection (#682)
Add a Stream view to the Audit Log that leads with a four-tile signal rail (events, actors, failure rate with inline sparkline, peak hour) and presents the feed grouped by day with severity dots, relative times, and inline anomaly callouts. The existing Table view is preserved behind a toggle for power users. Anomaly flags are computed at read time against strictly prior history and returned on demand via ?with_anomalies=1: - unusual_hour: hour outside the actor's central 7-day window - new_ip: IP unseen for this actor in the last 30 days - first_seen_actor: no prior history in the 30-day window New /audit-log/stats endpoint returns the signal-rail aggregates over 24h/7d/30d windows; stats are derived from a single 30-day scan. |
||
|
|
95278843cf |
feat(schedules): next-24h timeline + merge auto-update into schedules (#681)
* feat(backend): add stack update-preview endpoint for readiness board Adds GET /api/stacks/:stackName/update-preview that returns per-image semver diff, bump classification, and a stack-level summary powering the Auto-Update readiness board. - New UpdatePreviewService parses compose images, inspects local digests, fetches remote digests and tag lists, and finds the highest compatible semver tag. - Major bumps are flagged blocked until human review; unknown bumps rank below real semver so they cannot mask a major. - Rollback target is reconstructed through parseImageRef to preserve registry ports and drop the Docker Hub library/ prefix. - Registry helpers (httpGet, auth token, digest, tag list, ref parse) are extracted into registry-api.ts and shared with ImageUpdateService. - 28 Vitest cases cover parse, selection, bump math, digest rebuilds, blocked policy, and rollback target construction. * feat(schedules): next-24h timeline, merge auto-update crud, add readiness board Replace the flat task table with a Timeline view as the default, showing the next 24 hours of scheduled work across four lanes (Restart, Update, Scan, Prune) with a live now rail and per-firing pills. The All tasks tab preserves the existing CRUD surface. Merge Auto-update Stack into Schedules as a first-class action and replace the standalone Auto-Update Policies view with a per-stack Readiness board that surfaces version diffs, risk tags, changelog previews, and rollback targets sourced from the stack update-preview endpoint. |
||
|
|
ec7620675e |
feat(app-store): editorial hero, category rail, security scan signal per tile (#679)
* feat(app-store): editorial hero, category rail, security scan signal per tile Rework the App Store view into an editorial layout with a 180px category rail, a featured-template hero, and compact tiles that surface star counts and vulnerability scan status at a glance. The deploy sheet splits into Essentials (one-click deploy) and Advanced (full port/volume/env control) tabs. Backend enriches /api/templates with scan_status, scan_cve_count, and a featured flag computed from the highest-star template with recorded stars. Scan lookups use a single batched SQL query to avoid N+1 round-trips. * fix(app-store): split firstSentence helper into its own module The firstSentence helper lived alongside the TemplateLogo component, which violates react-refresh/only-export-components — a file that exports a component must not also export non-component values, or Fast Refresh cannot establish an HMR boundary. Move the helper into appstore/util.ts and update the two import sites. |
||
|
|
5f6fdfcba8 |
feat(resources): lead with reclaimable disk banner and per-tab landings (#678)
Replaces the stacked bar + legend with a reclaim-first layout: an amber hero banner surfaces the total reclaimable bytes and breakdown (unused images, stopped containers, dangling volumes) with a one-click review & prune CTA; a three-tile treemap replaces the stacked bar with proportional areas for Sencho-managed, External, and Reclaimable; and the Volumes tab gets a two-card landing highlighting the largest volumes by size and recently changed ones. |
||
|
|
748ba46669 |
feat(dashboard): status masthead, unified gauges, stack health sparklines (#676)
* feat(dashboard): status masthead, unified gauges, stack health sparklines Rework the home dashboard around a single cyan-railed status masthead that carries the health state word, node meta, and reasons inline. The resource block collapses into one strip with a CPU hero sparkline, memory and disk gauge bars, and a network tile whose sparkline is built from per-container byte-counter deltas. Stack health becomes an 8-column mono table with row tinting, an uptime column sourced from the oldest running container's creation time, and a per-stack 10-minute CPU sparkline. Historical charts pick up a cyan gradient and an amber peak marker. A shared Sparkline primitive backs all of the above. * docs(dashboard): refresh screenshot for phase B layout * fix(dashboard): anchor sparkline bucketing to latest metric timestamp The cpuHistory, netHistory, and cpuPeakLabel memos called Date.now() inside useMemo, which violates react-hooks/purity: the rule fires because re-renders can produce different bucket boundaries from the same inputs. Derive a historyEndAt anchor from the newest metric sample in the polled series and thread it through to ResourceGauges. |
||
|
|
5bb4b01953 |
feat: auto-heal policies for unhealthy containers (#671)
* feat(db): add auto_heal_policies and auto_heal_history schema and CRUD Adds two new SQLite tables (auto_heal_policies, auto_heal_history) to DatabaseService.initSchema() and exposes CRUD methods: getAutoHealPolicies, getAutoHealPolicy, addAutoHealPolicy, updateAutoHealPolicy, deleteAutoHealPolicy, recordAutoHealHistory, getAutoHealHistory, incrementConsecutiveFailures, resetConsecutiveFailures, setPolicyEnabled. Also adds AutoHealPolicy and AutoHealHistoryEntry TypeScript interfaces. * feat(events): track health-status duration and expose state accessors - Add healthStatus and unhealthySince fields to InternalContainerState - onHealthStatus now records unhealthySince timestamp on first transition to unhealthy, and clears it when the container recovers or restarts - onStart resets both fields so a restarted container begins from 'starting' - Add listContainerStates() and getContainerState() public accessors for use by the upcoming AutoHealService evaluator * fix(auto-heal): key allowlist in updateAutoHealPolicy, cascade delete, extract ContainerHealthSnapshot * feat: add AutoHealService evaluator singleton Polls every 30 s, matches containers to enabled policies via Compose labels, and restarts containers that have been unhealthy beyond the configured threshold. Enforces cooldown, per-hour rate cap, and recent-user-action suppression; auto-disables policies after repeated consecutive failures. Also adds DockerEventManager.getService() accessor required by the evaluator. * fix(auto-heal): prune stale restartTimestamps, guard undefined policy id - Prune restartTimestamps entries for containers no longer running after each container list fetch, preventing unbounded map growth from dead container IDs. - Guard against policies with undefined id at the start of the per-policy loop; warn and skip rather than proceed with a non-null assertion. - Extract handleAutoDisable private helper to bring executeHeal under 30 lines and isolate the auto-disable side-effect sequence. - Move ContainerInfo type to module scope. * feat: add auto-heal API routes and wire AutoHealService lifecycle Registers five REST endpoints under /api/auto-heal/policies (list, create, patch, delete, history) with requirePaid + requireAdmin guards and Zod validation. Wires AutoHealService.start()/stop() into the server startup and graceful-shutdown blocks alongside MonitorService. * test: add AutoHealService and DatabaseService auto-heal unit tests - 15 unit tests for AutoHealService.shouldHeal covering all decision branches (healthy state, duration threshold, user-action suppression, cooldown, rate limiting, and correct skipReason values) - 13 integration tests for DatabaseService auto-heal CRUD: policy round-trip, stack-name filter, partial update, cascade delete, history ordering/limit, consecutive failure counters, and setPolicyEnabled toggle * fix: log AutoHealService shutdown errors consistently * fix(api): requireAdmin-first guard order and try/catch on auto-heal routes * feat(ui): add StackAutoHealSheet component * feat(ui): add Auto-Heal context menu item to EditorLayout * fix(ui): StackAutoHealSheet label, token, a11y, and useEffect fixes - Rename 'All services in stack' to 'All services' in combobox options and placeholder - Replace text-green-600 with text-success design token in actionColorClass - Add htmlFor/id pairs to all four numeric form inputs for accessibility - Inline fetch logic into useEffect, removing stale closure risk and eslint-disable comment - Remove now-unused fetchPolicies and fetchServices standalone functions - Update 'Auto-disable after' label to 'Auto-disable after (failures)' for clarity - Add toast.error in policy fetch failure path; services fetch silently skips as before * docs: add auto-heal-policies feature documentation * test(e2e): add auto-heal policies CRUD spec * fix(docs): correct auto-heal-policies nav position in docs.json |
||
|
|
8e7a567f69 |
feat: pilot agent outbound-mode for remote nodes (#667)
* feat: pilot agent outbound-mode for remote nodes Adds a second mode for managing remote nodes: the agent dials an outbound WebSocket tunnel to the primary, so the remote host no longer needs an inbound port, a reachable URL, or its own TLS certificate. Works behind NAT, residential routers, and corporate firewalls. The primary multiplexes HTTP and WebSocket requests over a single tunnel via a hybrid JSON + binary frame protocol, bridged through a per-tunnel loopback server so existing proxy and upgrade handlers route pilot-mode nodes identically to proxy-mode ones. Enrollment uses a single-use 15-minute pilot_enroll JWT exchanged for a long-lived pilot_tunnel credential on first connect. Proxy mode continues to work unchanged and both modes are supported side-by-side. * test(e2e): switch to proxy mode before asserting api_url field Remote nodes default to Pilot Agent mode, which hides the api_url input. The SSRF-validation tests need proxy mode, so the helper now selects Distributed API Proxy after picking Remote type before asserting the field is visible. * fix(e2e): wire Combobox id prop so node-mode selector resolves The Combobox trigger button had no id, leaving its Label orphaned and making getByRole name-based lookups fail. Adding id to the primitive, passing id="node-mode" from NodeManager, and updating the E2E helper to use #node-mode fixes both the a11y regression and the CI timeout. |
||
|
|
2fce1d3baf |
fix(security): server-driven pagination for scan history (#661)
Scan history fetched a fixed 200 most-recent rows and paginated them client-side, so older scans silently fell off mature nodes where baselining is most valuable. The list now fetches one page at a time via offset, with status=completed and imageRefLike filters applied server-side. Search input is debounced to avoid per-keystroke fetches. |
||
|
|
29ed0524c1 |
feat(security): severity-aware scheduled scan notifications (#654)
Enrich scheduled vulnerability scan completion notifications with per-severity CVE counts so recipients can triage from the message body alone. Expose the scan action in the schedule creation UI, require an explicit node_id, and harden fire-and-forget alert dispatches so a failing webhook cannot crash the scheduler. Notification body now reports scanned/skipped/failed counts plus critical/high/medium totals aggregated across fresh and cached scans, reflecting the current node posture rather than only what was newly scanned on this run. |
||
|
|
12bbf86dc4 |
feat(security): export scan results as SARIF 2.1.0 (#652)
Adds a new SarifExporter service that builds a SARIF 2.1.0 document from the stored scan findings (vulnerabilities, secrets, misconfigs). Rule IDs are namespaced to avoid collisions in a flat result list. Suppressions carry through as SARIF suppressions[] entries so GitHub code scanning and Defender for Cloud see the same accepted status shown in the UI. Exposed via GET /api/security/scans/:id/sarif, admin + paid-tier gated to match the SBOM export precedent. A SARIF button appears in the scan sheet next to SBOM and CSV for paid tiers. |
||
|
|
a95bf1ff33 |
feat(security): secret and misconfiguration scanning (#651)
Extends Trivy scans with secret detection in image filesystems and misconfiguration scanning for Compose stacks. Adds tabs to the scan drawer for vulnerabilities, secrets, and misconfigs. Secret matches are redacted server-side (first 8 chars + ellipsis) before storage. - TrivyService: --scanners vuln,secret for images; trivy config for stacks - DB: scanners_used/secret_count/misconfig_count cols; secret_findings, misconfig_findings tables; cache key scoped by scanners - Routes: POST /security/scan accepts scanners array (requirePaid when secret requested); POST /security/scan/stack; GET .../secrets and .../misconfigs (paid-tier reads) - UI: tabs in VulnerabilityScanSheet; scan-options dropdown on images; Scan config button on stack header |
||
|
|
732fc95415 |
feat(security): fleet-replicated CVE suppression list (#650)
Operators can accept known-benign findings once and have Sencho filter them out of scan drawers, comparison views, and other read surfaces. Suppressions replicate from the control instance to every remote node. * New cve_suppressions table with a COALESCE-based unique index so NULL scope slots collide the way users expect * Admin + paid-tier CRUD routes; writes are rejected on replicas * Read-time filter enriches vulnerability details and compare payloads without mutating stored counts * Settings > Security panel for managing rules, per-CVE suppress action in the scan drawer, dimmed rows with a shield-off indicator * Vitest unit tests for the filter (glob, expiry, specificity) and route tests (auth, tier, replica, UNIQUE conflict) |
||
|
|
708d15b2b3 |
feat(fleet): replicate scan policies across managed nodes (#649)
Scan policies now propagate from the control Sencho instance to every registered remote. The control is the source of truth; replicas render rules read-only with a managed-by-control banner. Pushes fire on every policy write, record per-node success and failure on a new fleet_sync_status table, and use node_proxy Bearer tokens exclusively so only sibling Senchos can apply incoming sync payloads. Policy scope now travels as a string identity (api_url or a local sentinel) so node-scoped rules evaluate correctly on each target. |
||
|
|
e660d2a658 |
feat(scheduler): notify on scheduled scan completion (#646)
Scheduled scan tasks now dispatch a completion alert through the existing notification system: info when every image scanned cleanly, warning when one or more images failed. The alert includes the task name and the run's scanned/cached/failed summary so operators do not need to open the task history. |
||
|
|
61bac08027 |
feat(security): one-click managed Trivy install (#643)
* feat(security): one-click managed Trivy install Add a Vulnerability Scanner card to Settings, Security with install, update, uninstall, and auto-update controls (Admiral-only). The installer downloads a verified Trivy release into the existing data volume at /app/data/bin/trivy and defaults the cache to /app/data/trivy-cache, so no host mounts or extra env vars are required. Detection probes the managed path, a TRIVY_BIN override, and the host PATH, distinguishing managed vs host installs. A daily scheduled check surfaces available Trivy updates, installs them automatically when opted in, and dedupes notifications per version. * fix(frontend): silence react-hooks/set-state-in-effect in useTrivyStatus The initial status fetch and managed-source update check both call setState from the effect body. Match the existing pattern used in useDashboardData / SSOSection and disable the rule at the call site. |
||
|
|
dc8370f5a4 |
fix(security): harden Trivy scan lifecycle, logging, and docs (#639)
* fix(security): harden Trivy scan lifecycle, logging, and docs - Call TrivyService.initialize() at startup so capability state is accurate before first request; add periodic re-detect to the scheduler so newly installed Trivy binaries light up without a restart. - Add markStaleScansAsFailed sweep (+ idx_vuln_scans_status index) to recover any scan row left in_progress after a crash or timeout; sweep runs before the paid-tier gate so every tier self-heals. - Split scanImage persistence into beginScan/finishScan so the manual scan route owns a single code path and can return a scanId synchronously while work continues asynchronously. - Validate image refs on /api/security/scan and /sbom via new utility; defense-in-depth against shell-metacharacter payloads. - Dispatch a warning-level alert when a post-deploy scan fails so the operator has a user-visible path to the failure instead of a silent log. - Share DIGEST_CACHE_TTL_MS and severity ordering across service and route layers; remove dead invalidateDetection(). - Add [Trivy:diag] logging gated behind developer_mode for support diagnostics; production logs unchanged. - Frontend: defensive toast fallback chain, sr-only SheetDescription, and a truncation badge when the 500-item detail fetch is capped. - Tests: extend trivy-service and vulnerability-db suites; add image-ref and severity unit tests. - Docs: expand vulnerability-scanning troubleshooting with recovery, re-detect, and diagnostic-log guidance; link Dockerfile comment to trivy-setup. * fix(security): drop unnecessary escape in image-ref forbidden-char regex |
||
|
|
c9cd6990d2 |
feat(images): Trivy-powered vulnerability scanning (#635)
* feat(images): Trivy-powered vulnerability scanning Scan container images for known CVEs via Trivy. On-demand scanning and severity badges are available on every tier; scheduled scans, scan policies, SBOM generation, and scan history are gated to Skipper+. - New TrivyService (binary detection, per-image scan, SBOM, digest cache) - Three new tables: vulnerability_scans, vulnerability_details, scan_policies - 12 routes under /api/security (scan, results, summaries, SBOM, policies, compare) - Post-deploy async scans wired into all five deploy paths, with a per-deploy opt-out toggle in the App Store deploy sheet - "scan" action type added to SchedulerService for fleet-wide recurring scans - Frontend: severity badges in Resources Hub with animated cursor detail, scan results drawer with vulnerability table and filters, and a new Security section in Settings for scan policy CRUD - Policy threshold violations dispatch a warning or critical alert based on the policy's block_on_deploy flag; deploys themselves are never blocked * fix(security): compute scan age in useEffect to satisfy react-hooks/purity |
||
|
|
6890224903 |
fix(sso): harden Custom OIDC provider and SSO configuration (#630)
- Add Host header injection validation for OAuth callback URL derivation when SSO_CALLBACK_URL is not set (extracted into shared getSSOBaseUrl helper) - Add startup warning when OIDC providers are enabled without SSO_CALLBACK_URL - Add diagnostic logging for claim fallback, email changes on re-login, and admin seat limit enforcement (gated behind Developer Mode) - Add Custom OIDC environment variables to .env.example - Fix stale AdmiralGate comment in SSOSection.tsx - Fix .env.example section header referencing removed Admiral tier gate - Fix docs: correct Custom OIDC display name default, replace nonexistent DEBUG=true env var reference with Developer Mode toggle - Add tests for role enforcement (viewer 403), API token scope denial, OIDC claim edge cases, Custom OIDC test connection, callback error params - Refresh SSO settings screenshots |
||
|
|
7c6df0aa5d |
feat: add Custom OIDC provider and move SSO to Community tier (#626)
* feat: add Custom OIDC provider and move SSO to Community tier Add a generic Custom OIDC provider that works with any spec-compliant OIDC identity provider (Keycloak, Authentik, Authelia, Zitadel, KanIDM, Pocket ID, etc.) via standard discovery. Supports configurable claim mapping for User ID, Username, and Email fields to handle non-standard providers. Move all SSO functionality (LDAP and OIDC) from the Admiral tier to the Community tier so every user has access to identity provider integration. Backend: add oidc_custom to AuthProvider type, extend SSOService with claim mapping fields and env-var seeding, add oidc_custom to route validation, remove requireAdmiral guards from SSO config endpoints. Frontend: add Custom OIDC card with Display Name, Issuer URL, and claim mapping fields to SSOSection; add KeyRound icon on login page; remove AdmiralGate wrapper and lock icon from SSO settings nav. Tests: update tier guard expectations, add oidc_custom authorize/config/ provisioning tests and claim mapping coverage. All 992 tests pass. Docs: add Custom OIDC configuration reference, provider-specific setup examples, troubleshooting section, and updated screenshots. * fix: settings dialog close button overlap and combobox styling Reposition the close button in Settings Hub above the scroll area so it stays fixed when content scrolls. Increase dialog height to accommodate the growing number of setting sections. Fix combobox trigger styling to match Input component tokens (border-glass-border, bg-input) and eliminate the gap between trigger and dropdown list (top-full -mt-px). Apply the same fixes to multi-select-combobox for consistency. Add items-start to the Scopes/Default Role grid so the combobox aligns with the adjacent input field. Add showClose prop to DialogContent for consumers that need custom close button placement. Update SSO doc screenshots at 1920x900. |
||
|
|
4722028904 |
feat(mfa): UX hardening — auto-submit, paste tolerance, low-codes warning, dev-mode diagnostics (#620)
* feat(mfa): auto-submit 6-digit TOTPs and normalize pasted backup codes Match the UX every major MFA prompt has (GitHub, GitLab, 1Password): the challenge screen and every code-entry dialog now submit automatically once the sixth TOTP digit lands, and the backup-code input accepts pastes with smart-dashes, trailing whitespace, or mixed case without silently truncating the value. Also caps the backup-code input at the correct 11 characters (10 plus a single separator) instead of 12. Shared normalization helpers live in frontend/src/lib/mfa.ts so the challenge and the three account-settings dialogs stay in lockstep. * feat(mfa): warn users when backup codes run low The Account & Security card silently showed a dim count of backup codes remaining, which meant users could drift toward zero without noticing until their phone was already lost. The card now surfaces a warning tone with an alert icon when 1 or 2 codes remain, and swaps to a dedicated destructive warning card with a "Regenerate now" action when the user has used every code. * feat(mfa): gate diagnostic logs behind developer mode Reuses the existing isDebugEnabled() gate so operators investigating a 2FA support ticket can flip Developer Mode on to get per-branch diagnostics (login path taken, replay check outcome, failure counter after a verify, replay-table purge counts), and flip it back off when they are done. Standard lifecycle logs stay on by default: enrolment completed, 2FA disabled, backup codes regenerated, admin reset, SSO bypass toggled, lockout engaged. Nothing that could reveal a TOTP code, base32 secret, backup-code cleartext, or partial-auth JWT is ever logged. * test(mfa): cover drift, invalid formats, lockout recovery, and paste normalization Backend: a TOTP generated for a window that has already slid out is rejected, malformed backup codes (too short, non-alphanumeric, 11-char alphanumeric that matches no hash) all increment failed_attempts, a successful verify clears a below-threshold failure streak, a successful verify after locked_until has passed clears the lockout, a second enroll/start overwrites the prior pending secret, and the backup-code normalizer treats en-dash/em-dash/figure-dash with stray whitespace the same as the canonical form. E2E: low-backup-codes warning renders in the warning tone and the exhausted-codes state flips to the dedicated warning card, a 6-digit TOTP auto-submits without a button click, and a backup code pasted without the separator still signs in. * docs(mfa): auto-submit, paste guidance, and expanded troubleshooting Document that the challenge screen submits automatically on the sixth digit, that backup codes accept the separator and any case, and that the Account & Security card nudges at low code counts. Expands the troubleshooting section with entries for lost or exhausted backup codes and adds a short note to the admin guide about surfacing auth diagnostics via Developer Mode. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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). |
||
|
|
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
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |