mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-05 16:37:46 +00:00
3e01daf76fbcfc0775555e30e99d8151959119de
306 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3e01daf76f |
feat(stack): per-stack activity timeline with actor attribution (#852)
* feat(stack): per-stack activity timeline with actor attribution Adds an Activity tab to the Stack Anatomy panel showing a timestamped event log for each stack: deploys, restarts, starts, stops, and image updates, attributed to the user who triggered them or 'system' for automated actions. Backend: - Extends notification_history with actor_username column (idempotent migration) and a partial composite index on (node_id, stack_name, timestamp DESC) for efficient per-stack lookups. - NotificationService.dispatchAlert() accepts an optional actor that is written to the new column. - Success-side dispatchAlert calls added after deploy, bulkContainerOp (start/stop/restart), and update handlers in routes/stacks.ts so user-initiated operations are recorded, not just failures. - New GET /api/stacks/:stackName/activity?limit&before endpoint with stack:read permission gate and cursor-based pagination. Frontend: - StackAnatomyPanel grows an Anatomy / Activity tab pair using the existing Tabs primitive. - StackActivityTimeline fetches the initial 50 events, paginates on demand, and prepends live events arriving over the existing WS notifications stream without duplicates. - NotificationPanel bell dropdown suppresses user-initiated success events (start/stop/restart/deploy/update triggered by a real user), keeping the tray focused on alerts and system events. * docs(stack): add stack activity timeline feature page and internal arch docs * fix(test): add actor_username to notification-routing history assertions dispatchAlert now passes actor_username to addNotificationHistory after the activity timeline PR added the column. Update the two exact-match assertions that were failing because the expected object shape was missing this field. |
||
|
|
3da0aa6036 |
chore: migrate repository URLs from AnsoCode/Sencho to studio-saelix/sencho
Updates all hardcoded GitHub repository references across 21 files: - package.json: repository URL, bugs URL, homepage, description, author - CONTRIBUTING.md: bug report template URL - SECURITY.md: advisory URL, cosign cert-identity regexp - .github/CODEOWNERS: @AnsoCode -> @studio-saelix/maintainers - .github/workflows/ci.yml: repositories scope (Sencho -> sencho), docs-sync git URL - .github/workflows/cla.yml: path-to-document URL - .github/workflows/docker-publish.yml: cosign verify comment - frontend/**/*.tsx: issues and changelog links (3 components) - frontend/public/.well-known/security.txt: Contact and Policy URLs - security/vex/sencho.openvex.json: @id field - docs/openapi.yaml: license URL - docs/docs.json: navbar and footer GitHub links (5 instances) - docs/security.mdx: advisory and SECURITY.md links - docs/reference/verifying-images.mdx: repo link + cosign regexp + legacy identity note - docs/reference/contact.mdx: issues, LICENSE, advisory, policy, CoC links - docs/reference/security-advisories.mdx: releases link - docs/operations/verifying-images.mdx: cosign regexps and VEX download URL (6 instances) - docs/operations/upgrade.mdx: releases links (2 instances) - backend/src/utils/version-check.ts: GitHub Releases API endpoint CHANGELOG.md intentionally excluded (release-please managed). Legacy cosign identity note added for pre-migration image verification. |
||
|
|
7d4390a7e4 |
fix(backend): resolve ts-node dynamic import and TS2322 narrowing errors (#839)
* fix(backend): resolve ts-node dynamic import and TS2322 narrowing errors * fix(backend): disable triple-slash reference lint error in convert.ts |
||
|
|
219dee720e |
fix(convert): resolve TS7016 and TS2322 for composerize dynamic import (#837)
* fix(convert): resolve TS7016 and TS2322 for composerize dynamic import * fix(convert): remove triple-slash reference banned by ESLint |
||
|
|
46fae21e67 |
perf(backend): parallelize pruneManagedOnly removals (#830)
DockerController.pruneManagedOnly removed managed volumes, networks, and images one at a time inside a serial for-await loop. Each remove call hits the Docker daemon over the Unix socket with a synchronous-from-the-caller's-perspective HTTP round-trip, so a prune over N items took the sum of N round-trips. The daemon handles concurrent removes fine for these resource types. Wrap each loop in Promise.all so wall time tracks the slowest single remove rather than the sum. The existing per-item try/catch keeps the partial-failure semantics: a single resource that fails to delete logs and continues; the rest still get removed. JavaScript single-threading makes the shared reclaimedBytes counter safe under the parallel awaits. |
||
|
|
2000653fb4 |
perf(test): build baseline DB once via vitest globalSetup (#829)
Each test file's setupTestDb() previously re-ran the full DatabaseService init path: initSchema (~30 CREATE TABLE IF NOT EXISTS), 14 idempotent migrate*() methods, a bcrypt hash, and the admin / settings seed inserts. With 82 files this was a meaningful slice of the per-fork cold-start cost. Move the build into a vitest globalSetup that runs once before any worker boots. The baseline DB lands at a fixed temp path; each worker's setupTestDb copies it into the per-file data dir, opens the copy via DatabaseService.getInstance() (re-running the same idempotent init as a no-op pass), then UPDATEs the seeded local node's compose_dir to match the per-file COMPOSE_DIR (the baseline recorded /app/compose because COMPOSE_DIR was unset when the seed fired in initSchema; without realigning, file-routes tests 400 on path traversal). TEST_JWT_SECRET moves from a per-file randomBytes assignment to a fixed constant in a new testConstants module so the value the baseline seeds matches the value test files import for direct token signing. setupTestDb re-exports it for back-compat with the existing import sites. A baseline-less measurement on the same machine flakes 30 of 82 files at the no-cap baseline; with this baseline copy, the same tree drops to 0-3 failures (the residual environmental Windows flakes) and ~47-52 s wall time. |
||
|
|
04f35fdf22 |
perf(backend): mark AWS SDK clients as optional dependencies (#821)
@aws-sdk/client-ecr and @aws-sdk/client-s3 each pull in dozens of
@smithy/* and middleware-* transitive packages but only fire when an
operator configures an ECR registry or cloud backup respectively. Move
both to optionalDependencies so the package classification matches
their runtime role and operators who never use either feature can run
`npm ci --omit=optional` for a ~150 MB-slimmer image.
The default Dockerfile install (`npm ci --omit=dev`) keeps shipping
the SDKs, so default installs are unchanged. The dynamic imports in
CloudBackupService.loadS3Sdk and RegistryService.fetchEcrToken now
catch a missing-module failure and throw a wrapped Error whose
message names the recovery path (`reinstall without --omit=optional`)
and whose cause propagates the original module-not-found error for
debugging.
Bumps tsconfig.json's target and lib to ES2022 so `new Error(msg,
{ cause })` is typed; Node 25 already supports this at runtime.
|
||
|
|
14c25a6dbc |
perf(backend): lazy-load @aws-sdk/client-s3 in CloudBackupService (#820)
@aws-sdk/client-s3 pulls in dozens of @smithy/* and middleware-* transitive packages. Cloud backup is a Skipper+ opt-in feature and the bulk of installs never configure it, but the eager top-level import meant every cold start parsed the whole SDK regardless. Wrap the import in a load-and-cache helper, make buildS3Client async, and have it return both the S3Client and the SDK namespace so each caller constructs commands from the same lazily-loaded module. The pattern matches the existing dynamic import of @aws-sdk/client-ecr in RegistryService and the lazy-loaded composerize and isomorphic-git in PR #819. Tests use vi.mock('@aws-sdk/client-s3', ...) returning named exports, which works the same way for dynamic imports as it did for the static ones. |
||
|
|
329b4ec4e2 |
perf(backend): lazy-load composerize and isomorphic-git (#819)
Both modules are opt-in:
- composerize (~2 MB) is only used by /api/convert when a user pastes a
docker run command into the converter UI.
- isomorphic-git plus isomorphic-git/http/node (~5 MB combined) only fire
when a stack is created from a Git source.
Previously each was imported at module scope, parsing the whole package on
every cold start regardless of whether the feature was used. Wrap them in
small load-and-cache helpers so the first call resolves the module via
Node's loader and every subsequent call returns the cached reference.
The pattern matches the existing dynamic import of @aws-sdk/client-ecr in
RegistryService. Existing tests using vi.mock('isomorphic-git', ...) and
vi.mock('isomorphic-git/http/node', ...) keep working without changes
because dynamic and static imports share the same module registry.
|
||
|
|
279ec62dff |
perf(backend): replace docker system df shell-out with dockerode API (#818)
MonitorService.evaluate() forked the docker CLI every 30s and
walked the human-readable Reclaimable strings ("1.196GB", etc.)
with a regex to compute the janitor threshold check. The Docker
Engine API returns raw byte counts, and the existing
DockerController.getDiskUsage() already wraps it for images,
containers, and volumes. Extend that helper with reclaimable
build-cache bytes so MonitorService can sum the four categories
in one call.
Drops the child_process / promisify imports from MonitorService and
removes about 30 lines of stdout parsing. Also widens the explicit
return type of getDiskUsageClassified so the new fields aren't
silent runtime additions.
|
||
|
|
5cf4323511 |
perf(backend): batch audit_log inserts into a buffered transaction (#817)
Every mutating /api/* request runs an individual INSERT into audit_log which serializes against other writers under burst load (SQLite's single-writer model). Buffer the writes in DatabaseService and flush them in a single transaction either every second or once the buffer reaches 100 entries, whichever comes first. Read paths (getAuditLogs, getAuditLogsInRange, cleanupOldAuditLogs) drain the buffer first so callers always see a consistent view, which keeps the existing test pattern of insert-then-read working. Graceful shutdown flushes before db.close() so no entries are lost on clean exit. The 1s flush timer is unref'd so the buffer cannot keep the process alive on its own. The CLI resetMfa script flushes explicitly before returning since it exits before the timer fires. |
||
|
|
18cf2e65e8 |
perf(backend): parallelize independent startup initializers (#816)
The boot path awaited SelfUpdateService.initialize, DockerEventManager.start, and TrivyService.initialize one at a time even though none of them depend on each other. Group them into a single Promise.all so total cold-start time is the slowest one rather than the sum. Also convert the inner `docker compose version` probe in SelfUpdateService.initialize from execFileSync to execFileAsync. Without that, the synchronous spawn would block the event loop for up to 5 seconds and silently serialize the other two members of the parallel block, defeating the parallelization win for in-container deployments. The synchronous service starts (Monitor, AutoHeal, ImageUpdate, Scheduler, Mfa) are grouped together up front. They schedule timers whose first ticks fire 5+ seconds out, so they safely run alongside the awaited block. |
||
|
|
61a7e43d82 |
perf(proxy): cache LicenseService tier headers for the proxy hot path (#815)
The remote-node HTTP proxy and WebSocket forwarder read getTier() + getVariant() on every forwarded request to set the Distributed License Enforcement headers. Each call hits system_state 5+ times. Add a 30-second cached snapshot inside LicenseService and route every license_status write through a new private setLicenseStatus() helper so activate, deactivate, validate, and the auto-demote paths inside getTier() all invalidate the cache. Routing all license_status writes through one chokepoint also closes a latent drift window: the self-heal paths in getTier() (trial expired, offline grace exceeded, subscription expired) used to mutate state silently and now invalidate the cache the same way explicit license events do. The TTL becomes a safety net against any future write that bypasses the helper, not a load-bearing freshness bound. Existing 44 license and distributed-license tests pass unchanged. |
||
|
|
836e384d17 |
perf(backend): cache global_settings reads in DatabaseService (#814)
getGlobalSettings() runs a SELECT * on every call and is hit from 22 files, including the auth middleware (every authenticated request), the WebSocket upgrade handler (every connection), and the debug-mode gate (every diagnostic log line). Cache the result inside the service on first read and invalidate on updateGlobalSetting(). The cached snapshot is Object.freeze'd and the public return type is now Readonly<Record<string, string>> so accidental mutations are caught at compile time. The settings GET handler that delete'd private keys now takes a defensive shallow copy first. The 5-second TTL cache in utils/debug.ts is now redundant and removed; the service-level cache is strictly fresher (invalidates on write rather than going stale for up to 5s). |
||
|
|
ae8211c0b4 |
fix(fleet): forward main node tier to remote config fetch and hide local-only fields (#811)
The /fleet/configuration endpoint fetched remote node config via a direct backend-to-backend fetch that omitted the distributed license headers (x-sencho-tier, x-sencho-variant). Remote nodes evaluated their own Community tier and returned locked: true for Webhooks, Scanning, and Backup even when the main node held a Skipper/Admiral license. Forward the same tier/variant headers that remoteNodeProxy already injects so tier gates on remote nodes honour the main node's license. MFA and Backup are also hidden for remote node cards in the Status tab: - MFA is a user session feature managed on the main node; remote nodes are accessed via node_proxy Bearer tokens with no userId, so the value was always "Not set" and provided no useful information. - Backup (Sencho Cloud Backup) runs fleet-wide from the main node and captures all nodes' compose files; remote nodes never configure it independently, so showing it there was misleading. Removing both fields from remote cards keeps the grid at 6 items (3 even pairs) and eliminates stale or irrelevant data. |
||
|
|
6ac02c792a |
fix(backend): use URL parser for registry scheme + template host check (#808)
Two small hardenings flagged by CodeQL:
- routes/registries.ts: drop the redundant startsWith block-list and rely
solely on URL parsing + protocol allow-list. The startsWith pass was
unreachable defense (any non-http/https scheme already fails the
protocol check below it) and was tripping js/incomplete-url-scheme-check.
- services/TemplateService.ts: replace the .includes('api.linuxserver.io')
substring match with new URL(registryUrl).hostname comparison. The
substring form would mis-classify a malicious admin-set URL like
https://evil.example/api.linuxserver.io/... as the LSIO registry and
apply the LSIO response parser to its payload. Hostname compare closes
that.
Behavioral parity for the happy path: every previously-accepted URL still
parses; the LSIO branch still triggers when the hostname is exactly
api.linuxserver.io.
|
||
|
|
4e5ba17710 |
refactor(backend): sanitize user input before logging to close CRLF injection (#807)
* refactor(backend): sanitize user input before logging to close CRLF injection
Adds a small sanitizeForLog helper that strips CR, LF, tab, and ASCII
control characters (0x00-0x1F, 0x7F) from a value before it is embedded
in a console.log/warn/error/debug call. Wraps every call site where a
user-controlled value (req.params, req.body, req.query, or a value
derived from them) flows into a log message.
Closes the bulk of the open CodeQL alerts in this family:
- 96 js/log-injection
- 28 js/tainted-format-string
The helper is in backend/src/utils/safeLog.ts. Routes still pre-validate
input at the request boundary; this is the second line of defense and
gives static analyzers a sanitizer they can trace through. JSON
responses, Docker filter labels, and other non-log call sites are
intentionally left unwrapped.
* refactor(backend): printf-style format strings for tainted-log call sites
CodeQL's js/tainted-format-string rule flags template literals in the first
arg of console.X when any interpolated value is user-controlled, regardless
of whether each value is sanitized inline. The canonical mitigation is to
use a static format string and pass values as positional args.
Converts the 28 flagged template literals to printf-style ("%s") format
strings, with sanitizeForLog applied to each positional arg. Also fills in
the log-injection wraps on 9 sites where a user-controlled value was
missed in the first sweep (agents, fleet, gitSources, imageUpdates,
GitSourceService).
No behavior change at runtime. Node's util.format substitutes %s tokens
identically to template-literal interpolation.
* fix(backend): wrap nodeId/snapshotId in fleet restore debug log
CodeQL flagged the unwrapped numeric args even though they cannot
contain control chars in practice. Apply the sanitizer for taint-flow
recognition.
|
||
|
|
77f27b4bf9 |
refactor(backend): defensive path validation in FileSystemService (#802)
Adds two private helpers and routes the legacy stack-scoped methods through them so every fs call has a name + path check immediately above the call site: - assertWithinBase(filePath): throws INVALID_PATH if the path resolves outside this.baseDir. Wired into the bare readFile/writeFile/access wrappers and hasComposeFile(dir). - resolveStackDir(stackName): throws INVALID_STACK_NAME if the name fails isValidStackName, then asserts the joined path is within base. Wired into getComposeFilePath, saveStackContent, envExists, getEnvContent, saveEnvContent, createStack, deleteStack, backupStackFiles, restoreStackFiles. Routes still pre-validate at the request boundary; this is the second line of defense and gives static analyzers a guard they can trace through. The existing resolveSafeStackPath used by file-explorer methods is unchanged (it adds symlink-escape detection on top). The duplicate inline regex in createStack is removed because resolveStackDir now performs the same check via isValidStackName. |
||
|
|
f037b435f3 |
refactor(backend): use stacksRouter.param for stackName validation (#799)
* refactor(backend): use stacksRouter.param for stackName validation Registers a router-level param validator on :stackName so the 400 'Invalid stack name' guard runs once per route entry instead of being duplicated in every handler. Removes ~22 inline isValidStackName checks across the stacks router (deploy, down, env, files, services, update-preview, rollback, backup, etc.). Validation now fires before per-handler tier and permission checks, which matches the standard input-validate-first pattern. The body-field validators in POST / and POST /from-git remain inline because they operate on req.body, not the route param. Closes #752 * fix(stacks): remove unused stackName local in upload multer wrapper The multer middleware wrapper for POST /:stackName/files/upload no longer needs a local stackName binding now that param-level validation handles the check. Removes the stale assignment that ESLint flagged and corrects the leftover indentation on the requirePaid line. |
||
|
|
4c352c74c8 |
refactor(backend): hoist parseIntParam helper to utils (#798)
Adds backend/src/utils/parseIntParam.ts with a shared parseIntParam helper that writes a 400 'Invalid <label>' response and returns null on non-numeric route params. Consolidates the parseInt + isNaN + 400 shape that was inlined or duplicated across multiple routers. Updated: - routes/fleet.ts: replaced the local parseIdParam wrapper. - routes/autoHeal.ts: replaced the local parsePolicyId wrapper. - routes/notifications.ts: replaced parseRouteId wrapper plus an inline notification-id site. - routes/apiTokens.ts, routes/labels.ts, routes/registries.ts, routes/scheduledTasks.ts, routes/users.ts: replaced inline copies. Out of scope (route handlers without an existing isNaN check, kept intentionally untouched to avoid introducing new 400 responses): alerts, nodes, webhooks, and several user-routes handlers that rely on a downstream 404 instead. Closes #748 |
||
|
|
add3abaece |
refactor(backend): replica guard helper for security routes (#797)
* refactor(backend): extract replica guard helper for security routes
Adds blockIfReplica(res, resource) in middleware/fleetSyncGuards.ts and
replaces six inline FleetSyncService.getRole() === 'replica' checks
across the security policies and CVE suppressions endpoints.
Error responses now use a uniform shape:
403 { error: 'Cannot modify <resource> on a replica instance.
Connect to the primary.', code: 'REPLICA_READ_ONLY' }
The new code field gives callers a stable discriminator without
matching prose.
Closes #750
* test(suppressions): match stable REPLICA_READ_ONLY code instead of prose
The replica guard helper exposes a stable code field for callers to
discriminate without grepping the human-readable error string. Switch
the replica-rejection assertion to use that code so the test no longer
breaks when the unified error template wording is tuned.
|
||
|
|
1747de1962 |
refactor(backend): extract bulkContainerOp helper for stack lifecycle routes (#796)
Collapses the three near-identical /:stackName/restart, /:stackName/stop, and /:stackName/start handlers in routes/stacks.ts into a single bulkContainerOp helper. Preserves the asymmetric notifyActionFailure behavior (restart and stop notify, start does not). Closes #751 |
||
|
|
8460ae9ede |
refactor(backend): hoist severity Sets to utils/severity.ts (#795)
Replaces five inline new Set([...]) literals scattered across routes/security.ts and routes/fleet.ts with two shared constants (FINDING_SEVERITIES, POLICY_SEVERITIES) exported from utils/severity.ts. Pure refactor: no error response or status code changes. Closes #749 |
||
|
|
38a9f277c6 |
feat(stacks): add optional volume prune to delete confirmation (#788)
The Delete Stack dialog now includes an opt-in checkbox to also remove associated Docker volumes when the stack is deleted. The checkbox is unchecked by default and resets to unchecked on every open. Backend: DELETE /stacks/:name accepts ?pruneVolumes=true and calls pruneManagedOnly for volumes labeled with the stack project name after bringing the stack down. Prune failure is non-fatal and logged; the delete proceeds regardless. |
||
|
|
dcf8794047 |
feat(app-store): sort grid by stars and rotate featured weekly (#787)
Grid templates are now sorted by star count (descending) so popular apps surface naturally rather than appearing in registry fetch order. Featured hero rotates weekly among the top 5 starred apps instead of always pinning the single highest-starred entry. Rotation is seeded by week number so all nodes show the same featured app throughout a given week. Registries with no star data gracefully skip the featured hero and preserve their natural ordering. |
||
|
|
d7d8f9bfe8 |
feat(dashboard): replace 24h charts with Configuration Status and Recent Activity (#785)
* feat(dashboard): replace 24h charts with Configuration Status and Recent Activity The 24-hour CPU/Memory area charts summed per-container metrics normalized to each container's CPU quota, producing numbers that bore no honest relationship to host load. The live ResourceGauges strip already shows accurate host-level stats, making the historical charts both inaccurate and redundant. This commit replaces that row with two side-by-side cards: - **Configuration Status**: aggregates every toggleable feature on the active node (notification agents, alert rules, routing rules, auto-heal, auto-update, webhooks, scheduled tasks, MFA, SSO, vulnerability scanning, cloud backup, and alert thresholds) into a single at-a-glance card. Tier-locked rows display an upgrade indicator instead of a value. Each row is clickable and navigates to the relevant settings section. Data refreshes every 60 s and immediately on state-invalidate events. - **Recent Activity**: lists the ten most recent notification-history events for the active node (deployments, image updates, auto-heal actions, scan findings, cloud backup events, system notices) with category icons and relative timestamps. Refreshes every 30 s. New backend endpoints: - GET /api/dashboard/configuration - per-node feature status with locked/ requiredTier markers so the frontend renders upgrade chips without extra calls. The endpoint sits after authGate and before the remote proxy so remote-node requests are transparently forwarded. - GET /api/dashboard/recent-activity?limit=N - thin wrapper over DatabaseService.getNotificationHistory. - GET /api/fleet/configuration - fleet-wide fan-out using the same Promise.allSettled dead-node-tolerant pattern as /fleet/overview. Exposed as the new "Status" tab on the Fleet page (after Snapshots). Shared utilities: - visibilityInterval and formatCount extracted to frontend/src/lib/utils.ts so the three polling hooks and two components share a single copy. * docs(dashboard): fix stale alt text referencing removed historical charts |
||
|
|
71d164cf9e |
chore(deps): bump the all-npm-backend group across 1 directory with 10 updates (#783)
* chore(deps): bump the all-npm-backend group across 1 directory with 10 updates Bumps the all-npm-backend group with 10 updates in the /backend directory: | Package | From | To | | --- | --- | --- | | [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1028.0` | `3.1037.0` | | [axios](https://github.com/axios/axios) | `1.15.0` | `1.15.2` | | [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | `12.8.0` | `12.9.0` | | [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.3.2` | `8.4.1` | | [openid-client](https://github.com/panva/openid-client) | `6.8.2` | `6.8.3` | | [otplib](https://github.com/yeojz/otplib/tree/HEAD/packages/otplib) | `12.0.1` | `13.4.0` | | [eslint](https://github.com/eslint/eslint) | `10.2.0` | `10.2.1` | | [typescript](https://github.com/microsoft/TypeScript) | `6.0.2` | `6.0.3` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.58.1` | `8.59.0` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.4` | `4.1.5` | Updates `@aws-sdk/client-ecr` from 3.1028.0 to 3.1037.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-ecr/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1037.0/clients/client-ecr) Updates `axios` from 1.15.0 to 1.15.2 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2) Updates `better-sqlite3` from 12.8.0 to 12.9.0 - [Release notes](https://github.com/WiseLibs/better-sqlite3/releases) - [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v12.8.0...v12.9.0) Updates `express-rate-limit` from 8.3.2 to 8.4.1 - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.3.2...v8.4.1) Updates `openid-client` from 6.8.2 to 6.8.3 - [Release notes](https://github.com/panva/openid-client/releases) - [Changelog](https://github.com/panva/openid-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/panva/openid-client/compare/v6.8.2...v6.8.3) Updates `otplib` from 12.0.1 to 13.4.0 - [Release notes](https://github.com/yeojz/otplib/releases) - [Commits](https://github.com/yeojz/otplib/commits/v13.4.0/packages/otplib) Updates `eslint` from 10.2.0 to 10.2.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.2.0...v10.2.1) Updates `typescript` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.2...v6.0.3) Updates `typescript-eslint` from 8.58.1 to 8.59.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.0/packages/typescript-eslint) Updates `vitest` from 4.1.4 to 4.1.5 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest) --- updated-dependencies: - dependency-name: "@aws-sdk/client-ecr" dependency-version: 3.1037.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: axios dependency-version: 1.15.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: better-sqlite3 dependency-version: 12.9.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: express-rate-limit dependency-version: 8.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: openid-client dependency-version: 6.8.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: otplib dependency-version: 13.4.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-npm-backend - dependency-name: eslint dependency-version: 10.2.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: typescript dependency-version: 6.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: typescript-eslint dependency-version: 8.59.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: vitest dependency-version: 4.1.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend ... Signed-off-by: dependabot[bot] <support@github.com> * fix(mfa): migrate otplib API to v13 The v13 release removed the singleton authenticator export and renamed HashAlgorithms to a string union type. Switch to the OTP class with generateSync/verifySync for synchronous operation, passing per-call options instead of setting global instance state. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: SaelixCode <dev@saelix.com> |
||
|
|
03f91cd5bb |
feat(cloud-backup): mirror fleet snapshots to S3-compatible storage (#782)
* feat(cloud-backup): mirror fleet snapshots to S3-compatible storage
Add an Admiral-tier Cloud Backup feature that replicates every fleet
snapshot to off-site storage, with two provider modes that share the
same `@aws-sdk/client-s3` code path:
- Sencho Cloud Backup: zero-config, 500 MB allowance backed by
Cloudflare R2, provisioned via the sencho.io worker against the
user's Lemon Squeezy license.
- Custom S3 (BYOB): any S3-compatible bucket (AWS, MinIO, Backblaze
B2, Wasabi, R2 with own keys), with credentials encrypted via
`CryptoService` before storage.
API-triggered snapshots upload fire-and-forget so the UI returns
immediately; scheduled snapshots block on the upload so the task's
success/failure reflects cloud durability. Object keys include the
instance_id segment to prevent collisions when the same Admiral
license is activated on multiple Sencho instances.
* fix(cloud-backup): drop ES2022-only Error cause arg breaking ES2020 build
The backend tsconfig pins lib to ES2020. The two-argument
`Error(message, { cause })` form requires ES2022, so tsc rejected it
with TS2554. Revert to single-argument throw to match the
convention used elsewhere in the backend services.
|
||
|
|
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.
|
||
|
|
dd9d33813b |
feat(deploy-logs): opt-in deploy progress modal with structured log rows (#779)
* feat(notifications): dispatch deploy_failure alert on stack action errors
* feat(terminal): add onReady and onMessage callback props
* feat(deploy-logs): add DeployLogContext with runWithLog API
* feat(deploy-logs): add DeployLogPanel bottom drawer with resize and minimize
* feat(deploy-logs): wire DeployLogContext to App and EditorLayout action runners
* test(deploy-logs): add E2E test for deploy log panel open, failure, and minimize
* docs(deploy-logs): add user-facing and internal architecture docs
* feat(deploy-logs): redesign as opt-in modal with structured log rows
Replace the full-width bottom drawer (DeployLogPanel) with a centered
modal that streams structured log output for deploy, stop, restart,
update, install, and Git apply operations. The modal is disabled by
default; users opt in from Settings -> Appearance.
Core changes:
- New DeployFeedbackContext with runWithLog() API: if opt-in is off,
silently bypasses the UI so all call sites degrade to the existing
toast behavior without code changes.
- composeLogParser.ts: pure parser that strips ANSI escapes and
classifies compose output into stage badges (PULL, BUILD, CREATE,
START, STOP, DOWN, WARN, ERR, LOG). 15 unit tests.
- StructuredLogRow.tsx: memoized row with timestamp, stage badge, and
message. Error rows get a rose left rail; warn rows get a tinted bg.
- DeployFeedbackModal: Dialog-based, max-w-640px/max-h-70vh, elapsed
timer, auto-close 4s on success (hover cancels), persistent on
failure. Raw xterm output collapsible in footer.
- DeployFeedbackPill: minimized state anchored top-right, survives
navigation, click restores modal.
- Wires App Store install (action: install), Git apply (action: deploy),
and Git pull (action: update) in addition to the existing EditorLayout
actions.
- Fixes Terminal.tsx WS URL in generic mode (was connecting to root path
not proxied by Vite; now uses /ws).
- Settings: adds "Show deploy progress modal" checkbox to Appearance.
- Docs: renames deploy-logs.mdx to deploy-progress.mdx; updates
internal architecture doc.
* fix(deploy-logs): connect Terminal in generic mode and move pill to bottom-center
Terminal was passed stackName which routes it to the stack logs WS
(container stdout). In that mode onReady is never called, so the
deployStarted gate never resolves and the compose command never runs.
Remove stackName so Terminal uses generic WS mode, which calls onReady
on open and streams compose output.
Also reposition the minimized pill from top-right to bottom-center
(fixed bottom-6 left-1/2 -translate-x-1/2) per UX feedback.
* docs(deploy-logs): update pill position to bottom center
* test(deploy-logs): rewrite E2E spec for deploy feedback modal
The old spec targeted the removed bottom-drawer DeployLogPanel and used
the wrong field name when calling POST /api/stacks (sent 'name' but the
endpoint reads 'stackName'), causing every test to fail with a 400 before
any UI assertions ran.
Fixes:
- POST /api/stacks body now uses 'stackName' matching the API contract
- All locators updated to target the new DeployFeedbackModal and
DeployFeedbackPill components (data-testid attributes added)
- Added enableDeployFeedback helper to opt-in via localStorage before
each test that expects the modal (feature is off by default)
- Added opt-in OFF test to confirm the modal is suppressed when disabled
- Minimize/expand test now asserts the pill appears and contains the
stack name before clicking to restore the modal
* test(deploy-logs): fix compose file write endpoint in E2E helper
createStackViaApi was calling PUT /api/stacks/:name/files/docker-compose.yml
which does not exist. The correct endpoint is PUT /api/stacks/:name with
{ content } in the body.
* test(deploy-logs): use addInitScript to persist opt-in across reloads
The opt-in flag was set via page.evaluate before setupDeployStack, which
calls page.reload() and loginAs (a second navigation). Although localStorage
should persist across same-origin reloads, the React tree was reading
'false' on remount in CI. Switching to addInitScript guarantees the
localStorage value is set before any page script on every navigation, so
useDeployFeedbackEnabled's useState initializer always sees the right
value when React mounts.
* test(deploy-logs): verify localStorage and re-dispatch event before deploy
Adds syncDeployFeedbackState() called right before each deploy click in
the ON tests. It both verifies localStorage is set (failing the test
loudly with a clear message if not) and re-dispatches the
SENCHO_SETTINGS_CHANGED event to defeat any stale React state after
navigation. If the modal still does not appear with the assertion green,
the issue is downstream of localStorage and we have a clear signal.
* test(deploy-logs): wait for React re-render after dispatching opt-in event
After syncDeployFeedbackState dispatches SENCHO_SETTINGS_CHANGED, React
schedules the state update but does not flush it synchronously. The
click that follows can fire against the stale closure where isEnabled is
still false, so runWithLog takes its early-return path and the modal
never opens. A 200ms wait is enough to let React commit the new state
before the next interaction.
* test(deploy-logs): wait for stack file fetch before clicking deploy
deployStack() in EditorLayout returns early at 'if (!selectedFile)'
without calling runWithLog. selectedFile is set inside loadFile() after
GET /api/stacks/:name resolves. The previous setup clicked the stack in
the sidebar and immediately asked the test to click Deploy, racing the
fetch. CI backend logs confirmed no deploy POST ever fired for the ON
tests, while the OFF test passed only because it asserts non-existence.
Now setup awaits both the stack click and the file response together,
then verifies the action bar's deploy button is visible before returning.
* test(deploy-logs): wait for network idle and capture browser logs
Adds a networkidle wait plus a 500ms settle after the stack click so
React commits selectedFile and any follow-up env/container/backup
fetches drain before the deploy click. Also mirrors browser console
errors and pageerrors into the Playwright output so the next failure
ships with the React stack trace instead of just a 'modal not visible'
message.
* test(deploy-logs): temporary debug logging in runWithLog
Adds a console.log at the entry of runWithLog so we can see in CI logs
whether it is being called and what isEnabled value the closure has.
Also widens the test's console capture to include these debug lines.
This is diagnostic only and will be removed once the root cause of the
modal-not-opening-in-CI failure is identified.
* test(deploy-logs): debug log at deployStack entry to trace click path
Adds console.log at the first line of deployStack handler so we can
confirm in CI whether the click is reaching it at all and what
selectedFile/isStackBusy resolve to. Combined with the existing
runWithLog debug logs, this isolates whether the modal failure is in
deployStack guarding out, runWithLog early-returning, or something
else entirely.
* test(deploy-logs): drop filter, log every browser console msg
The previous filter only emitted error/warning plus the deploy-feedback
substring. The deploy-feedback debug logs never appeared, so we don't
yet know whether the log itself is firing. Remove the filter so the
full console stream shows up in CI.
* test(deploy-logs): app-level console log to verify capture pipeline
If even an unconditional log at App component render time does not
appear in CI browser logs, then the console capture listener is broken
or the dispatched logs are being filtered upstream of Playwright. This
isolates whether the issue is in the production code or the test
harness.
* test(deploy-logs): use testid locator for stack action button
Replaces the regex-based getByRole locator (/Deploy|Start/i) with
getByTestId('stack-deploy-button'). The regex matched something other
than the actual deploy button: backend logs proved no deploy POST ever
fired, and instrumentation confirmed neither deployStack nor runWithLog
ran on click despite the test claiming success.
Adds data-testid='stack-deploy-button' to both the Restart and Start
button branches in EditorLayout's action bar so the same locator works
whether the stack is running or not.
Also drops the temporary debug console.log entries in deployStack,
runWithLog, and App, and restores the test's console listener filter
to only emit error and warning messages.
* test(deploy-logs): park cursor in corner so auto-close countdown fires
After clicking the deploy button, the cursor lands inside the centered
modal. The modal pauses its 4s auto-close countdown on hover, so the
HAPPY test was waiting for a close that never happened. page.mouse.move
to (0,0) parks the cursor outside the modal before the success banner
appears, letting the countdown complete.
* test(deploy-logs): drop redundant loginAs after page.reload
page.reload preserves auth cookies, so the page lands back on the
dashboard without needing a fresh login. The loginAs call after reload
was racing on isLoginPage(): a transient login-page state during page
load made loginAs commit to filling #username, then the dashboard
committed and #username never came back. Playwright's auto-wait then
hung the fill until the test's 120s timeout, which also dragged later
stacks.spec tests down with collateral timeouts.
waitForStacksLoaded is enough to confirm we're on the dashboard with
the sidebar populated before clicking the new stack.
* test(e2e): make loginAs race-safe when login page is a false positive
isLoginPage() reports the page as a login screen if the Login button
locator reports visible at the moment of the check. Under CI load (more
real container deploys from the deploy-log-panel suite), the auth
context can render the login form for one paint, then redirect to the
dashboard. The original code committed to filling #username and hung
until the test timeout when the field was no longer there.
Now the login branch waits up to 2s for #username to actually appear
before filling. If it never appears, we fall through to the dashboard
check instead of hanging.
|
||
|
|
6986b927e3 |
feat(stacks): per-service start/stop/restart lifecycle actions (#778)
* feat(stacks): add per-service start/stop/restart lifecycle routes
Adds POST /:stackName/services/:serviceName/{start,stop,restart} routes
that operate on containers belonging to a single Compose service, using
the same Engine API pattern as the existing stack-level lifecycle routes.
Includes isValidServiceName validator and audit-summary entries for the
three new paths.
* test(stacks): add per-service action route tests
* test(stacks): fix test quality issues in service action tests
* feat(stacks): add per-service lifecycle menu to container cards
* fix(stacks): handle paused container state in service action menu
* docs(stacks): add per-service lifecycle actions documentation
* docs(stacks): add validation screenshots for per-service lifecycle actions
|
||
|
|
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. |
||
|
|
58df1a50b3 |
feat(auto-update): show pending image updates fleet-wide on the Auto-Updates page (#770)
Group readiness cards by node so updates pending on every reachable node are visible without having to switch the active node. Apply now targets the owning node directly, and Recheck fans out to every reachable node in parallel; per-node cooldowns are surfaced in the toast. Adds POST /image-updates/fleet/refresh and invalidates the fleet aggregation cache after auto-update execute so the next read reflects the new state immediately. A small banner appears under the hero when some online nodes did not respond within the request timeout. |
||
|
|
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.
|
||
|
|
a962654a3b |
fix(env): return empty body for missing .env files; surface non-OK responses cleanly (#767)
Previously, fetching the .env file for a stack with no env files at all returned a 404 with a JSON error body. The frontend's secondary loader (changeEnvFile) called res.text() without checking res.ok, which caused the error body to be stuffed directly into the editor as if it were file content. Two-part fix: Backend (routes/stacks.ts): - For the default GET /stacks/:name/env (no ?file= query) when the stack has no env files, respond 200 with an empty body and an X-Env-Exists: false header instead of 404. - For an explicit ?file= query that resolves to a missing file, keep the 404 (the caller asked for something specific). - Catch a TOCTOU ENOENT between access() and readFile() and return the same friendly empty-body shape, not a generic 500. Frontend (EditorLayout.tsx::changeEnvFile): - Check res.ok before reading the body. On a non-OK response, clear the editor content and surface a friendly toast instead of pasting the server's JSON error string into the file. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |