Commit Graph

954 Commits

Author SHA1 Message Date
Anso 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.
2026-04-28 01:16:07 -04:00
Anso 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.
2026-04-28 00:59:04 -04:00
Anso 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.
2026-04-28 00:33:17 -04:00
Anso 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.
2026-04-28 00:13:07 -04:00
Anso 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).
2026-04-27 23:45:25 -04:00
Anso 502ee83438 chore(backend): move @types/* to devDependencies (#813)
The seven @types/* packages contain only TypeScript declaration files,
which are erased at compile time and have no runtime use. Listing them
under dependencies kept them installed in the production image for no
benefit; they belong under devDependencies so `npm ci --omit=dev`
prunes them from the runtime install.

Packages moved: @types/compression, @types/cors, @types/dockerode,
@types/express, @types/http-proxy, @types/semver, @types/ws.
2026-04-27 23:21:45 -04:00
Anso 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.
2026-04-27 15:47:51 -04:00
Anso b5acfd8f58 feat(scaffold): fully automate release blog post publishing (#810)
* feat(scaffold): fully automate release blog post publishing

Eliminates the manual PR-review step from the release blog scaffold pipeline.
The workflow now commits the generated post directly to sencho-website main.

scaffold-release-post.mjs changes:
- Remove renderPrBody and all body_file handling
- Update findLastAnchor to accept any post with a version: field, not just
  category: release posts. This allows narrative retrospectives to anchor
  the 5-release window.
- Add buildHeadline: generates a post title from the top two added items and
  the total item count, e.g. "Sencho v0.69: Custom OIDC provider, Trivy
  scanning, and 12 more"
- Add buildDescription: auto-generates a ~160-char SEO description from the
  version range and top three added items
- Add calcReadingTime: estimates reading time from changelog word count
- Add coveredVersionsProse: formats "v0.65.0 through v0.69.0"
- Add updateMeta: inserts the new post into src/data/blog/meta.ts for
  Worker-side SEO meta injection
- Remove the tmpdir/os import (no longer needed)

release-blog-template.tsx.tmpl changes:
- Replace all TODO placeholders with template variables (__TITLE__,
  __DESCRIPTION__, __READING_TIME__, __COVERED_VERSIONS_PROSE__)
- Remove the screenshot placeholder section (auto-generated posts have no
  screenshot; the intro links to docs.sencho.io instead)

release-blog-scaffold.yml changes:
- Remove permission-pull-requests: write from app token (no PR created)
- Replace "Commit and open draft PR" step with a simple git commit + push to
  sencho-website main. No duplicate-check step needed.
- Include src/data/blog/meta.ts in the git add so the Worker SEO mapping
  stays in sync automatically.

* fix(ci): use escapeTsxString in updateMeta to handle backslashes in blog post title and description

CodeQL flagged two high-severity alerts: title and description written
into meta.ts via updateMeta were only single-quote-escaped, leaving
raw backslashes unescaped. A value containing a backslash followed by
a special character would produce a malformed escape sequence in the
generated TypeScript source. The escapeTsxString helper already handles
both cases; use it consistently instead of an open-coded replace.
2026-04-27 15:38:01 -04:00
sencho-quartermaster[bot] 5d376590f8 docs: refresh screenshots (#809)
Co-authored-by: AnsoCode <18150933+AnsoCode@users.noreply.github.com>
2026-04-27 15:06:14 +00:00
sencho-quartermaster[bot] 79753c6a37 chore(main): release 0.64.2 (#806)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com>
2026-04-27 11:04:33 -04:00
Anso 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.
2026-04-27 11:02:10 -04:00
Anso 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.
2026-04-27 10:47:23 -04:00
Anso 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.
2026-04-27 09:37:41 -04:00
Anso c18d3696e9 fix(deps): bump dompurify to 3.4.0 to resolve four advisories (#801)
Resolves four DOMPurify advisories all patched in 3.4.0:
- GHSA-39q2-94rc-95cp (ADD_TAGS function form bypasses FORBID_TAGS)
- GHSA-v9jr-rg53-9pgp (CVE-2026-41238: prototype pollution to XSS via
  CUSTOM_ELEMENT_HANDLING fallback)
- GHSA-h7mw-gpvr-xq4m (CVE-2026-41240: FORBID_TAGS bypass via function-based
  ADD_TAGS predicate)
- GHSA-crv5-9vww-q3g8 (CVE-2026-41239: SAFE_FOR_TEMPLATES bypass in
  RETURN_DOM mode)

Bump is on the npm overrides entry (dompurify is a transitive dep), which
forces the resolved version up the tree. Lockfile picks up 3.4.1 (latest
3.4.x). Frontend build clean, dev server boot clean, no runtime errors.
2026-04-27 09:37:09 -04:00
sencho-quartermaster[bot] 3f4f7c6135 docs: refresh screenshots (#805)
Co-authored-by: AnsoCode <18150933+AnsoCode@users.noreply.github.com>
2026-04-27 13:14:15 +00:00
sencho-quartermaster[bot] de50a2fab2 chore(main): release 0.64.1 (#804)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com>
2026-04-27 09:12:47 -04:00
Anso 06d9ef5904 fix(docker): build compose plugin from ./cmd, not ./cmd/compose (#803)
* fix(docker): build compose plugin from ./cmd, not ./cmd/compose

The compose-builder stage in Dockerfile was building ./cmd/compose, which
in docker/compose v5.1.2 is `package compose` (cobra command definitions),
not the CLI plugin entry point. `go build -o file pkg` against a non-main
package writes a Go archive; COPY and chmod accept it, but the kernel
cannot exec an ar-format file, so the Docker CLI plugin manager rejected
the binary and surfaced only as `docker: unknown command: docker compose`
in the v0.64.0 release smoke gate. The plugin's actual main package is at
./cmd (cmd/main.go calls plugin.Run from the cli-plugins framework), per
the upstream Makefile in v5.1.2.

Also fix the version ldflag to target the v5 module path: in v5 the module
moved from github.com/docker/compose/v2 to .../v5, so the previous
-X .../v2/internal.Version= silently no-op'd and `docker compose version`
would have reported an empty string even after the build target fix.

Add -trimpath and -s -w to match the upstream Makefile's release flags.

Add an inline ELF-magic sanity check inside the compose-builder stage so
this exact failure mode (non-main package emitting an archive) fails the
build at the producer instead of surfacing 200 lines later. Hex match via
`od -tx1` works on both busybox and GNU coreutils; the named-character
form (`od -c`) renders fields differently across the two.

Add a binary smoke step to the docker-validate PR job that mirrors the
release-time gate, so a regression in either the CLI or compose source
build is caught at PR review time rather than at tag-push time.

* fix(vex): match Trivy purl format for bundled docker/docker version

The compose plugin build target fix exposes a real consequence: now that
compose-builder produces a true ELF binary (rather than a Go archive),
Trivy can read the embedded Go module metadata and correctly reports the
bundled docker/docker dependency as `v28.5.2+incompatible`. The existing
VEX subcomponent ID omitted the `+incompatible` Go-module suffix, so
Trivy's purl matcher rejected the suppression and CVE-2026-34040 surfaced
in the docker-validate scan against the rebuilt image.

Update the @id to the literal version Trivy emits and bump the document
version per OpenVEX spec. The justification (compose acts as a CLI
client, never as a daemon, so the authz-hook path is unreachable) is
unchanged and continues to apply to v28.5.2+incompatible identically.

* docs(security): explain why CVE-2026-34040 cannot be upgraded out

A direct in-build upgrade attempt (`go get github.com/docker/docker@<sha
of docker-v29.3.1>`) fails with:

  invalid version: go.mod has post-v1 module path
  "github.com/moby/moby/v2" at revision f78c987a

moby/moby migrated its Go module path to `github.com/moby/moby/v2` on
the docker-29.x branch. The legacy `github.com/docker/docker` import path
is therefore frozen at v28.5.2+incompatible from Go's perspective; the
proxy.golang.org listing for that module path confirms no v29.x version
is resolvable. compose v5.1.2 (and v5.1.3, the latest tag at the time of
writing) both still import the legacy path, so the bundled docker/docker
library cannot be moved past v28.5.2+incompatible until upstream compose
migrates its imports.

Document the constraint in two places so a future maintainer (or future
me) does not re-attempt the same `go get` and arrive at the same dead
end:

- Dockerfile compose-builder stage: comment block above the build step.
- security/vex/sencho.openvex.json: expand the CVE-2026-34040 statement's
  impact_statement to spell out the upstream module-split blocker. The
  not_affected status (compose runs as a CLI client, never executes the
  daemon authz hook code path) is the principled triage and remains the
  correct OpenVEX call, not a deferred upgrade.

No build or runtime behaviour changes. Pure documentation enrichment.

* fix(security): mirror CVE-2026-34040 VEX statement in .trivyignore

Trivy reports `Some vulnerabilities have been ignored/suppressed` but
still fails the docker-validate scan on CVE-2026-34040, because the
OpenVEX statement's product identifier `pkg:oci/sencho` cannot match
the build-local image tag `sencho:pr-test`. Trivy does not generate an
OCI purl for images that lack a registry digest (aquasecurity/trivy
issue 9399), so VEX product matching is a no-op at PR and release scan
time. The dual-layer pattern documented in .trivyignore's header was
designed for exactly this case: VEX is the canonical, attested,
published triage record, and .trivyignore mirrors entries that scan-time
matching cannot resolve, with a comment pointing to the corresponding
VEX justification.

The CVE-2026-34040 OpenVEX statement is unchanged (status=not_affected,
justification=vulnerable_code_not_in_execute_path). The .trivyignore
entry is a build-time scan filter only, with a referencing comment so
auditors land on the VEX file as the source of truth. CVE-2026-39883
already follows this pattern and continues to.
2026-04-27 09:10:21 -04:00
sencho-quartermaster[bot] fb88ea41ed docs: refresh screenshots (#800)
Co-authored-by: AnsoCode <18150933+AnsoCode@users.noreply.github.com>
2026-04-27 04:56:43 +00:00
sencho-quartermaster[bot] d592bdd724 chore(main): release 0.64.0 (#728)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com>
2026-04-27 00:55:02 -04:00
Anso 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.
2026-04-27 00:39:42 -04:00
Anso 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
2026-04-27 00:39:28 -04:00
Anso 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.
2026-04-27 00:29:38 -04:00
dependabot[bot] 82894164e2 chore(deps): bump node from 22-alpine to 25-alpine (#792)
Bumps node from 22-alpine to 25-alpine.

---
updated-dependencies:
- dependency-name: node
  dependency-version: 25-alpine
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 00:22:44 -04:00
Anso 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
2026-04-27 00:05:34 -04:00
Anso 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
2026-04-27 00:05:12 -04:00
dependabot[bot] 4c4394b97e chore(deps): bump the all-actions group with 3 updates (#794)
Bumps the all-actions group with 3 updates: [github/codeql-action](https://github.com/github/codeql-action), [softprops/action-gh-release](https://github.com/softprops/action-gh-release) and [googleapis/release-please-action](https://github.com/googleapis/release-please-action).


Updates `github/codeql-action` from 3.35.2 to 4.35.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a...95e58e9a2cdfd71adc6e0353d5c52f41a045d225)

Updates `softprops/action-gh-release` from 2.6.2 to 3.0.0
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/3bb12739c298aeb8a4eeaf626c5b8d85266b0e65...b4309332981a82ec1c5618f44dd2e27cc8bfbfda)

Updates `googleapis/release-please-action` from 4.4.1 to 5.0.0
- [Release notes](https://github.com/googleapis/release-please-action/releases)
- [Changelog](https://github.com/googleapis/release-please-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/release-please-action/compare/5c625bfb5d1ff62eadeeb3772007f7f66fdcf071...45996ed1f6d02564a971a2fa1b5860e934307cf7)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
- dependency-name: googleapis/release-please-action
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 00:04:52 -04:00
dependabot[bot] 26bf86bcab chore(deps-dev): bump jsdom in /frontend in the all-npm-frontend group (#793)
Bumps the all-npm-frontend group in /frontend with 1 update: [jsdom](https://github.com/jsdom/jsdom).


Updates `jsdom` from 29.0.2 to 29.1.0
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v29.0.2...v29.1.0)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 29.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 23:45:06 -04:00
Anso e49553b18a ci: add CodeQL analysis and Docker ecosystem to Dependabot (#791)
* ci: add CodeQL analysis and Docker ecosystem to Dependabot

Add CodeQL workflow running javascript-typescript analysis weekly and on
every PR/push to main, using the security-extended query suite. All three
codeql-action steps are SHA-pinned.

Add the docker package-ecosystem to dependabot.yml so Dependabot opens
weekly PRs to bump Dockerfile digest pins (FROM node:22-alpine@sha256:...
and the golang builder stage) when upstream images are updated.

* ci(codeql): upgrade codeql-action to v3.35.2

v3.29.0 uses Node.js 20, which is deprecated on GitHub-hosted runners as
of April 2026, causing SARIF upload failures with "Resource not accessible
by integration." v3.35.2 runs on Node.js 24 and resolves the deprecation.

* ci(codeql): add actions:read permission for workflow run API

CodeQL v3.35.2 calls the GitHub Actions REST API to fetch workflow run
context for SARIF correlation, which requires actions:read. Without it,
the analyze step fails with "Resource not accessible by integration"
when calling /repos/{owner}/{repo}/actions/runs/{run_id}.
2026-04-26 23:30:43 -04:00
Anso 3668c71860 feat(security): add SBOM attestations, VEX document, and retire .trivyignore (#790)
* feat(security): add SBOM attestations, VEX document, and retire .trivyignore

Add OpenVEX triage document (security/vex/sencho.openvex.json) for the 5
residual CVEs vendored inside docker/compose v5.1.2 that were carried over
from the previous PR. All 5 are marked not_affected with justifications.

Configure Trivy in both CI and release workflows to consume the VEX document
via trivy.yaml so the same source of truth gates PR scans and release scans.
Delete .trivyignore, which is fully superseded by the VEX file.

Add two new release pipeline steps after image publication:
- CycloneDX 1.6 SBOM via anchore/sbom-action (also installs syft)
- SPDX 2.3 SBOM via syft directly (reuses OCI layer cache from prior step)
Both are attached as cosign OCI referrer attestations (keyless, OIDC-signed)
and uploaded as GitHub Release assets alongside the OpenVEX file.

Bump docker-publish.yml permissions from contents:read to contents:write,
required for softprops/action-gh-release to create Release assets.

Add docs/operations/verifying-images.mdx with copy-paste verification
commands for all supply-chain artifacts: signature, SLSA provenance,
CycloneDX SBOM, SPDX SBOM, OpenVEX, and Rekor entry. Update docs.json
navigation and expand the Supply chain security section in docs/security.mdx.
Add a Verifying Release Artifacts section to SECURITY.md.

* fix(vex): cover otel SDK CVE-2026-39883 in rebuilt Docker CLI binary

The rebuilt Docker CLI v29.4.0 vendors otel/sdk v1.42.0, which still
contains CVE-2026-39883 (BSD kenv PATH hijacking; fixed in v1.43.0).
docker-compose v5.1.2 vendors otel/sdk v1.38.0 separately. The original
VEX statement only covered the compose binary's location and version,
so Trivy's scan of /usr/local/bin/docker was not suppressed.

Add a second subcomponent entry for the CLI binary path with the
correct vendored version. The not_affected justification (BSD-only
code path; we ship linux/amd64 and linux/arm64 only) holds for both
binaries.

* fix(ci): use list form for vulnerability.vex in trivy.yaml

Trivy's config schema requires vulnerability.vex to be a list (mapped
to the multi-value --vex flag). The previous bare-string value was
silently dropped, so the OpenVEX document was never loaded and HIGH
findings already covered by VEX statements still failed the scan.

* fix(ci): mirror VEX CVE in .trivyignore for local-image scan

Trivy does not emit an OCI purl for locally-built images without a
RepoDigests entry (aquasecurity/trivy#9399), so OpenVEX product
matching against the CI build target sencho:pr-test resolves to no
artifact and every statement is silently dropped. The VEX document
remains the canonical triage record and is still attached as a cosign
attestation on the published image; this file just mirrors the single
CVE that surfaces on the local scan so CI does not block on a finding
already triaged in VEX. Updates the Trivy step comment to document
the relationship between the two files.
2026-04-26 23:21:11 -04:00
Anso dfa463e390 Add CodeQL analysis workflow configuration 2026-04-26 23:02:47 -04:00
Anso 7e4ea714c1 feat(security): rebuild Docker CLI/Compose from source, pin base image digests (#789)
* feat(security): rebuild Docker CLI/Compose from source, pin base image digests

Build Docker CLI v29.4.0 and Compose v5.1.2 from source against Go 1.26.2
to resolve 7 CVEs that were accepted in .trivyignore:

- CVE-2026-32280/32281/32282/32283/33810: Go stdlib x509/TLS and DNS
  issues in the upstream static CLI binary (compiled with Go 1.26.1).
  All fixed by rebuilding with Go 1.26.2.
- CVE-2026-33186: grpc 1.78.0 in Docker CLI. Eliminated from the SBOM
  by building with the patched Go toolchain and updated module graph.
- CVE-2026-33671: picomatch ReDoS in npm bundled with node:22-alpine.
  Resolved by removing npm/npx from the runtime image entirely (npm is
  only needed at build time).

Remaining 5 entries in .trivyignore are vendored deps inside Compose
v5.1.2 (buildkit, moby/docker, otel) that cannot be patched without an
upstream Compose release. These will be expressed as OpenVEX
not_affected statements in the follow-up PR (feat/security-sbom-vex).

Also in this commit:
- Pin all Dockerfile FROM lines to @sha256: digests (node:22-alpine,
  tonistiigi/xx, golang:1.26-alpine) to prevent silent base image
  changes between scan and publish.
- SHA-pin all GitHub Actions in docker-publish.yml and ci.yml that
  were previously referenced by mutable @vN tags.
- Add a binary version smoke test to confirm docker/compose produce
  expected output before the multi-arch push proceeds.

* fix(docker): fix cli-builder vendor mode and compose-builder cache path

docker/cli v29.4.0 uses CalVer and ships vendor.mod instead of go.mod,
so plain `go build` fails with "cannot find main module." Fix: copy
vendor.mod -> go.mod and vendor.sum -> go.sum before building, then
pass -mod=vendor so all deps come from the vendored tree with no network
access. Cache mount is not needed with vendor mode and is removed.

compose-builder used /root/go/pkg/mod as the cache mount target, but
golang:alpine sets GOPATH=/go, so the module cache lives at /go/pkg/mod.
The wrong path caused a silent cache miss on every build. Corrected.
2026-04-26 22:13:35 -04:00
Anso 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.
2026-04-26 18:56:30 -04:00
Anso 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.
2026-04-26 18:56:09 -04:00
Anso f94b2ce85c fix(sidebar): remove 1-hour staleness filter from activity ticker (#786)
Show the most recent stack event regardless of age. The ticker now
stays populated as long as any event arrived in the current session,
with formatTimeAgo reflecting the true elapsed time (e.g. '2h ago').
Idle state only appears when no events have arrived yet.

Also reduces the update interval from 30s to 10s so relative timestamps
stay responsive.
2026-04-26 18:55:49 -04:00
Anso 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
2026-04-26 18:42:28 -04:00
dependabot[bot] fdab44fe07 chore(deps-dev): bump otplib from 12.0.1 to 13.4.0 in the all-npm-root group (#723)
* chore(deps-dev): bump otplib in the all-npm-root group

Bumps the all-npm-root group with 1 update: [otplib](https://github.com/yeojz/otplib/tree/HEAD/packages/otplib).


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)

---
updated-dependencies:
- dependency-name: otplib
  dependency-version: 13.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: all-npm-root
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(e2e): migrate otplib imports to v13 API

The v13 release removed the singleton authenticator export. Switch to
the OTP class with generateSync, passing per-call options.

---------

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>
2026-04-26 16:34:01 -04:00
dependabot[bot] ccbf951ca8 chore(deps): bump the all-npm-frontend group across 1 directory with 11 updates (#784)
* chore(deps): bump the all-npm-frontend group across 1 directory with 11 updates

Bumps the all-npm-frontend group with 10 updates in the /frontend directory:

| Package | From | To |
| --- | --- | --- |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.8.0` | `1.11.0` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.2.2` | `4.2.4` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.5.2` | `25.6.0` |
| [eslint](https://github.com/eslint/eslint) | `10.2.0` | `10.2.1` |
| [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) | `7.0.1` | `7.1.1` |
| [globals](https://github.com/sindresorhus/globals) | `17.4.0` | `17.5.0` |
| [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` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.8` | `8.0.10` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.4` | `4.1.5` |



Updates `lucide-react` from 1.8.0 to 1.11.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.11.0/packages/lucide-react)

Updates `@tailwindcss/vite` from 4.2.2 to 4.2.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/@tailwindcss-vite)

Updates `@types/node` from 25.5.2 to 25.6.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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 `eslint-plugin-react-hooks` from 7.0.1 to 7.1.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.1/packages/eslint-plugin-react-hooks)

Updates `globals` from 17.4.0 to 17.5.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.4.0...v17.5.0)

Updates `tailwindcss` from 4.2.2 to 4.2.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/tailwindcss)

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 `vite` from 8.0.8 to 8.0.10
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite)

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: lucide-react
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: eslint
  dependency-version: 10.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: eslint-plugin-react-hooks
  dependency-version: 7.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: globals
  dependency-version: 17.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: tailwindcss
  dependency-version: 4.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: typescript-eslint
  dependency-version: 8.59.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: vite
  dependency-version: 8.0.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: vitest
  dependency-version: 4.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(lint): set new react-hooks compiler rules to warn

eslint-plugin-react-hooks@7.1.x added React Compiler compatibility
rules. Set them to warn rather than error so CI passes while we
adopt the patterns incrementally. The React Compiler itself is not
yet in use in this project.

---------

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>
2026-04-26 16:25:15 -04:00
dependabot[bot] 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>
2026-04-26 16:24:03 -04:00
dependabot[bot] f15d0a94e8 chore(deps-dev): bump postcss from 8.5.8 to 8.5.10 in /backend (#762)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.10.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.10)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 15:56:13 -04:00
dependabot[bot] a9b0d70396 chore(deps): bump postcss from 8.5.8 to 8.5.10 in /frontend (#760)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.10.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.10)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 15:56:10 -04:00
dependabot[bot] 95cb2242ca chore(deps): bump uuid and dockerode in /backend (#759)
Removes [uuid](https://github.com/uuidjs/uuid). It's no longer used after updating ancestor dependency [dockerode](https://github.com/apocas/dockerode). These dependencies need to be updated together.


Removes `uuid`

Updates `dockerode` from 4.0.10 to 5.0.0
- [Release notes](https://github.com/apocas/dockerode/releases)
- [Commits](https://github.com/apocas/dockerode/compare/v4.0.10...v5.0.0)

---
updated-dependencies:
- dependency-name: dockerode
  dependency-version: 5.0.0
  dependency-type: direct:production
- dependency-name: uuid
  dependency-version: 
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 15:56:03 -04:00
dependabot[bot] b0807fbb5e chore(deps): bump the all-actions group across 1 directory with 4 updates (#724)
Bumps the all-actions group with 4 updates in the / directory: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action), [actions/download-artifact](https://github.com/actions/download-artifact), [actions/create-github-app-token](https://github.com/actions/create-github-app-token) and [googleapis/release-please-action](https://github.com/googleapis/release-please-action).


Updates `aquasecurity/trivy-action` from 0.35.0 to 0.36.0
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/57a97c7e7821a5776cebc9bb87c984fa69cba8f1...ed142fd0673e97e23eac54620cfb913e5ce36c25)

Updates `actions/download-artifact` from 7 to 8
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

Updates `actions/create-github-app-token` from 3.0.0 to 3.1.1
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](https://github.com/actions/create-github-app-token/compare/f8d387b68d61c58ab83c6c016672934102569859...1b10c78c7865c340bc4f6099eb2f838309f1e8c3)

Updates `googleapis/release-please-action` from 4.4.0 to 4.4.1
- [Release notes](https://github.com/googleapis/release-please-action/releases)
- [Changelog](https://github.com/googleapis/release-please-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/release-please-action/compare/16a9c90856f42705d54a6fda1823352bdc62cf38...5c625bfb5d1ff62eadeeb3772007f7f66fdcf071)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
- dependency-name: actions/create-github-app-token
  dependency-version: 3.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: googleapis/release-please-action
  dependency-version: 4.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 15:56:01 -04:00
Anso 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.
2026-04-26 15:42:21 -04:00
Anso 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.
2026-04-26 13:05:19 -04:00
Anso 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.
2026-04-26 00:43:54 -04:00
Anso 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
2026-04-25 17:26:04 -04:00
Anso 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.
2026-04-25 16:11:13 -04:00
Anso 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
2026-04-25 14:58:50 -04:00
Anso 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.
2026-04-25 13:56:48 -04:00
Anso 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.
2026-04-25 13:55:07 -04:00