mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-10 10:49:35 +00:00
a3edee5e6a6c6319bb292f2e37d96aa3260acf3f
32 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a3edee5e6a |
feat: weekly UTC maintenance windows for mute rules (#1661)
* feat: add weekly UTC maintenance windows to mute rules
Let mute rules suppress only during recurring UTC windows, normalize
replica node identity, and fail-open when remotes lack schedule support
so older nodes never keep an all-day scheduled mute after a successful cleanup DELETE.
* fix: fail closed on corrupt mute schedules and clean invalid replicas
Empty or whitespace stored schedules no longer act as all-day mutes. Invalid schedules trigger remote DELETE cleanup, and the weekly-window form gains accessibility attributes plus component coverage.
* fix: require explicit repair before clearing a corrupt mute schedule
The suppression engine already fails closed on an unreadable stored
schedule (scheduleInvalid), but the frontend never surfaced that flag:
a corrupt rule looked identical to an ordinary unscheduled one, and
opening Edit then clicking Update sent an explicit schedule: null,
silently turning the corruption into a valid all-day mute. Add the
flag to the rule type, show an Invalid schedule badge on the card, and
block saving in the edit form until the operator explicitly touches
the weekly window (configures a new one, or toggles it to confirm the
clear).
* fix: correct contradictory toggle-sequence copy in schedule-repair toast
The blocking toast told operators to toggle the weekly window "off then
on" to confirm clearing a corrupt schedule, but the toggle starts off
for a corrupt rule, so that sequence leaves it on and trips the
no-selected-day validation instead. The correct, tested sequence is on
then off, matching the inline hint below the toggle. Also add a
regression test confirming the invalid-schedule save gate resets
cleanly across edit sessions on different rules.
* fix: enforce replica node_id and guard fleet sync against stale writes
Two hardenings to the suppression-rule fleet sync path found during
review: the /replica endpoint trusted the payload's node_id instead of
forcing it to null server-side, so a direct proxy-authenticated caller
could persist a scoped replica; and upsertNotificationSuppressionRuleReplica
overwrote unconditionally with no ordering check, so a delayed older
POST arriving after a newer one could downgrade the stored rule. Force
node_id to null on every replica write, and skip (with a warning log)
any incoming write whose updated_at is not newer than what's stored.
* test: assert the exact-tie updated_at case in the fleet sync stale-write guard
The staleness guard added in
|
||
|
|
63213c0960 |
feat: add service-scoped Compose update and restore (#1648)
* feat: add service-scoped Compose update and restore Allow updating or rebuilding one declared Compose service on multi-service stacks without recreating siblings, with recovery snapshots, health-gate observation, and prune holds for rollback images. Full-stack update paths and single-service UX stay unchanged. * fix: sanitize service-scoped update log messages for CodeQL * fix: address service-scoped update audit findings B-01 through B-07 * fix: complete service-scoped update audit metadata and surfaces * test: wrap Updates readiness tests for deploy-feedback context * fix: keep service recovery reachable without Deploy Progress Make failed service-gate recovery discoverable when Deploy Progress is disabled or dismissed, suppress stale image-scan notification side effects, normalize ComposeService line endings, and add focused regression coverage. * fix: resurface ContainersHealth density and expand on multi-service stacks Service grouping hid the summary strip and Compact/Detailed/Expand controls that still applied to multi-container stacks. |
||
|
|
35bb74425b |
feat: guide missing external network creation during deploy (#1645)
* feat: guide missing external network creation during deploy Detect missing external networks before Compose runs, prompt or auto-create safe bridge networks, and keep unsupported declarations blocked with trusted deploy provenance. * test: align deploy context and settings fixtures with missing-network gate Update caller spies, EffResource expectations, StacksSection save keys, and git-source spy cleanup so CI matches the new deployStack context and auto-create setting. * fix: drop unused renderError binding in missing-network resolver Satisfies no-unused-vars so backend ESLint CI passes; callers already key only on model presence. * fix: use HTTP-safe clipboard helper in missing-network dialog navigator.clipboard fails on plain HTTP LAN hosts; route copy actions through copyToClipboard so Docker and Compose copy buttons work on self-hosted instances. * fix: simplify missing-network dialog actions and copy label Drop the Compose snippet escape hatch, move secondary actions under More, and rename the terminal copy action to Copy create command so the footer is a clear Cancel / Create decision. |
||
|
|
381ed2a91f |
feat: add Admiral Hardened Build channel and business assurance surfaces (#1629)
* feat: add Admiral Hardened Build channel and business assurance surfaces Introduce Studio Saelix entitlement-backed Hardened Build switching, a single-flight image operation coordinator, Recovery Vault naming, Admiral Account settings, and typed Fleet update failures while preserving Community custom-repo and targetless pull-current updates. * fix: harden image-op paths and clear CI CodeQL/pilot flake Validate operation IDs before filesystem use, use hostname checks in Fleet fetch mocks, sanitize registry probe logs, and swallow expected TCP teardown errors in the pilot reverse-route post-handshake test. * fix: sanitize image-op docker config write and probe logs Allowlist-copy registry host keys and base64 auth before writing the temp DOCKER_CONFIG, and log registry probe failures with a fixed message so CodeQL no longer flags network-to-file and log-injection mediums. * fix: address Admiral Hardened Build audit blockers Expose imageChannel so hardened Fleet peers still POST for typed rejection, claim community updates before 202, terminalize helper failures, gate Hardened on paid, and align support/docs/e2e wording. * fix: terminalize image ops on helper survival and aborted claims * fix: prevent recreating persist from overwriting helper-exit failure * test: assert helper-exit failure lands before recreating persist * fix: keep current pointer when acknowledging a stale image operation |
||
|
|
d113004359 |
feat: add confirmed Take down stack action with optional volume removal (#1599)
* feat: add confirmed Take down stack action with optional volume removal Expose Take down in the stack header and sidebar with a confirmation dialog that runs compose down while keeping the stack definition on disk. Optional volume removal is gated by node capability and stack:deploy permission, with remote gateway preflight before proxying removeVolumes requests. Closes #1582 * fix: reset take-down volume checkbox when dialog closes * test: align getStackMenuVisibility assertions with showTakeDown key getStackMenuVisibility now returns a fifth lifecycle flag, showTakeDown, but three exhaustive toEqual assertions still listed only the prior four keys and failed. Add the expected showTakeDown value to each: true for the partial and exited running-stack cases, false for the self stack. * test: cover Take down visibility for running non-self stacks The getStackMenuVisibility assertions exercised the partial and exited branches and the self-stack guard, but not the raw === 'running' literal that drives showTakeDown for a normal running stack. Add a case so a regression dropping 'running' from that check is caught. * fix: drop Take down from header overflow and wire activity shortcut Remove duplicate Take down from More actions. Keep inline button when running, sidebar menu, and Cmd+ArrowDown. Record stack_taken_down in activity on successful POST /down. |
||
|
|
e12602091a |
fix: repin semver compose tags during fleet self-update (#1587)
* fix: repin semver compose tags during fleet self-update Fleet updates failed when docker-compose.yml pinned a semver tag because recreate reused the on-disk pin. Pull the target image first, rewrite semver pins via the update helper, and block digest or unresolved pins with fast 409s. * fix: update OFFLINE_META shape in capability and node-registry meta tests |
||
|
|
4a350e7a0a | feat: add Docker label audit across Fleet and Stack views (#1531) | ||
|
|
b65daf6845 |
feat: add notification suppression rules (#1525)
* feat: add notification suppression rules * fix: restore label routing and routing test mocks for suppression * fix: allow bell mute shortcuts for history-only notification categories Suppression rule validation used the routable category whitelist, which rejected history-only categories such as update_started that appear in the bell during stack updates. * feat: expand Mute Rules UX with compose-first entry points and activity badges * fix: add missing NodeContext mocks for notification suppression tests |
||
|
|
997a6bb79a |
fix: gate cross-node HTTP and stop-by-label on remote RBAC capability (#1509)
An older remote node ignores the forwarded actor-role header (running proxied requests as admin) and ignores the stop-by-label stack allowlist (stopping every label-matched stack). The control could neither detect nor prevent this on a mixed-version fleet. Instances now advertise a cross-node-rbac capability, and the control refuses to act when a remote lacks it: - HTTP proxy: a non-admin user's request is not forwarded to a remote that does not advertise the capability (fails closed when it cannot be determined). Admins are unaffected. - Stop-by-label: a real stop bound to a confirmed stack set is not sent to a remote lacking the capability; the node is reported as needing an upgrade. As defense in depth, a node whose results name stacks outside the confirmed set is failed rather than rendered as a clean stop. Separately, the stop's lock-contention path now reports every confirmed stack as a contention failure (including one that lost its label), so a confirmed stack is never silently dropped and the result is never empty. |
||
|
|
a698aaa926 |
feat: add per-stack project env file selection for Docker Compose (#1457)
* feat: add per-stack project env file selection for Docker Compose
Allow users to configure an ordered list of env files per stack that serve
as the project environment file(s) for Docker Compose ${VAR} interpolation.
The selected files are passed via repeated --env-file flags during all
compose commands.
Backend:
- Add stack_project_env_files table (node-scoped, ordered)
- Extend authoredComposeEnvFileArgs to emit --env-file for configured files
- Add GET/PUT /stacks/:name/project-env-files and /candidates endpoints
- Update resolveStackEnvSources to use configured files as interpolation source
- Update resolveAllEnvFilePaths to merge injection + interpolation sources
- Add discoverStackLocalEnvFiles for candidate discovery
- Extend backupStackFiles and snapshotStackFiles for project env files
- Add project-env-files capability to CapabilityRegistry
Frontend:
- Add project env file selector to EnvironmentPanel (capability-gated)
- Update EditorView banner to generic "project environment file" language
- Add project-env-files capability to capabilities.ts
Issue: #1454
* fix: add realpath validation, clear all stale backup files, reject nested paths
- authoredComposeEnvFileArgs: use fsPromises.realpath + isPathWithinBase
for symlink escape defense at use time
- backupStackFiles: clear ALL non-marker files from backup slot before
writing, not just PROTECTED_STACK_FILES (handles stale old.env)
- PUT project-env-files: reject paths containing / or \ (root-level
only, matching Compose auto-discovery behavior)
* fix: add getStackProjectEnvFiles to compose-service mock
The new authoredComposeEnvFileArgs calls getStackProjectEnvFiles
on the DatabaseService singleton. The compose-service mesh-override
tests mock that singleton without the new method, causing 6 failures.
Add getStackProjectEnvFiles: () => [] (empty = fall back to legacy
behavior, which is what these tests exercise).
* fix: add getStackProjectEnvFiles to remaining service mocks
The new authoredComposeEnvFileArgs calls getStackProjectEnvFiles,
which is missing from the mock in compose-images.test.ts (6 failures)
and image-update-service.test.ts (proactive fix).
* fix: apply inline path-injection barrier at fs sink for CodeQL
The PUT project-env-files route resolved paths via isPathWithinBase
before calling fsp.stat, but CodeQL does not credit a containment check
separated from the sink. Apply the canonical inline barrier pattern
(path.resolve + startsWith at the sink) used throughout the codebase.
* fix: resolve stackDir from the same canonical root as safePath
Prevents a containment bypass when the compose base directory is
a symlink: stackDir was previously joined from the unresolved
baseDir while the inline barrier used path.resolve(baseDir),
which could differ for symlinked paths. Now both stackDir and
safePath are resolved from a single canonical root, then each is
containment-checked against it.
* fix: remove unused isPathWithinBase import
The inline path-injection barrier refactor replaced isPathWithinBase
with an inline startsWith check at the fs sink, so the import is now
unused and fails ESLint no-unused-vars.
|
||
|
|
9ea2864d60 |
feat: per-stack storage inventory and portability guardrails (#1399)
* feat: per-stack storage inventory and portability guardrails Add a Storage tab to the stack Anatomy panel that derives a per-stack mount inventory (bind mounts, named/anonymous volumes, tmpfs, docker socket; read-only vs read-write; host-path existence, type, and owner) from the effective Compose model, and classifies the stack as Portable, Partially portable, Node-bound, or Unknown with the reasons behind it. - New GET /api/stacks/:stackName/storage route (stack:read, Community), served by an on-demand, non-persisted service that renders the effective model, probes within-stack bind sources (symlink-escape aware), and runs the deterministic portability classifier. - Extend the effective-model parser additively with a full per-mount inventory and service-level tmpfs, leaving the rule-facing binds/namedVolumes byte-identical for the existing preflight rules. - New anonymous-volume preflight finding. - Admin-visible "no recent snapshot" warning that reuses the existing hub-local snapshot-coverage endpoint, plus a static note distinguishing config snapshots from application-data backups. - Surface storage assumptions in the Stack Dossier markdown export. - Gate the tab behind a new compose-storage capability on both sides. * docs: phrase the Storage tab availability as current behavior Replace the "older Sencho version / until it is updated" wording in the Storage feature page with present-tense, capability-based phrasing. |
||
|
|
57a0856ffc |
feat(stacks): per-stack environment inventory and secret-safe guardrails (#1397)
* feat(stacks): per-stack environment inventory and secret-safe guardrails
Add an Environment tab to Stack Anatomy that derives a per-stack inventory
of environment variables from the compose files and env files. Each variable
shows its source, whether Compose interpolates it or injects it into a
container, and a status (present, missing, unused, duplicate, or shell-only),
plus likely-secret classification. The inventory works from variable names
only: a value is never read, returned, or logged, and a likely secret shows
presence only. A copy env checklist action exports names and status without
values.
Surface a missing required env_file as a Compose Doctor preflight finding,
and add an opt-in node setting that refuses a deploy or update when a
required ${VAR:?...} variable is unset or empty, before any backup, pull, or
up runs. Default off.
The Environment tab is capability-gated so it hides on older remote nodes.
* fix(stacks): harden env-file reader against a stat-then-open race
Open the env-file handle first and fstat the open handle instead of
stat-ing the path before opening, removing the check-then-use window in
readEnvFileKeys. Use a secure mkdtemp directory for the out-of-base test
path instead of a predictable name in the temp root.
* fix(stacks): resolve nested env_file paths per compose file, reconcile inline keys per service
Resolve each env_file relative to the directory of the compose file that
declared it, so a nested multi-file Git override (infra/prod.yml referencing
./prod.env) lands next to that file instead of the stack root. The root
compose file is unaffected, since its directory is the stack directory.
Reconcile inline environment provenance per service, so a key an override
removed from one service's effective env is not labeled compose-inline just
because another service injects the same name from a different source.
|
||
|
|
77f1611971 |
feat: Compose Network Inspector and exposure intent guard (#1360)
* feat: add Compose Network Inspector facts engine Render a stack's authored effective model and pair it with the live Docker snapshot to derive per-stack networking facts: project networks with external and internal flags, service-to-network membership and aliases, published ports with host-binding scope, network_mode, and extra_hosts, plus runtime drift (runtime-only attachments, foreign networks, and declared-but-unused or missing networks). Extend the effective-model parser with service network membership, extra_hosts, and label keys (key names only, never values), and add a key-space normalized network model with adapters from both the rendered model and the raw declared compose so the Inspector and drift share one comparison. Expose GET /api/stacks/:stackName/networking: advisory and read-only, it renders the authored model only and never returns or logs raw stderr, env values, or label values. * feat: store and edit per-stack and per-service exposure intent Add a stack_exposure_intent table (intent values constrained by a CHECK, unique per node, stack, and service) with DAO methods to read, upsert, clear one row, and clear all rows for a stack. The classification is stored independently of the generated networking facts so a later mismatch stays detectable; service rows are kept separately from the stack-level row (service ''). Expose GET and PUT /api/stacks/:stackName/exposure: GET requires read access, PUT requires edit access and validates the intent against the allowed set. Sending intent null clears that row, returning the scope to unset so a service inherits the stack intent again. Intent rows are cleared when the stack is deleted and when the owning node is removed, so a later same-named stack never picks up stale classification. * feat: add exposure-aware Compose Doctor findings Feed the Compose Doctor's effective-model context with the stored exposure intent (resolved into a stack-level value plus per-service overrides) and the dossier's documented access-URL ports, read fail-soft so a metadata read error skips these checks rather than failing the preflight. Add five deterministic findings on top of that context: - a service classified internal or same-node that publishes a host port (same-node tolerates a loopback bind), - a sensitive database or admin image published on all interfaces, - a port-publishing stack with no exposure intent set, - a published port not reflected in the documented access URLs, - reverse-proxy labels with no documented URL or reverse-proxy intent. The rules stay pure functions over the preflight context; the registry completeness test pins the new rule set. * feat: detect compose network drift in the drift ledger Extend the spatial drift engine with two network-level findings: a running container attached to a stack-owned or foreign network that compose does not declare (one finding per service), and a declared network that no running service uses or that is absent from the runtime (one stack-level finding, every network named by its resolved runtime name). The comparison reuses the same helper the Network Inspector uses, so the two surfaces never disagree. Network drift runs only when the stack has running containers and the runtime is reachable, preserving the existing missing-runtime, parse-error, and unreachable behavior. The findings persist through the existing drift ledger and surface on the Drift tab, which now labels the two new kinds. * feat: link a Docker network back to its owning stack Add a cross-component open-stack event and make the owning-stack badge on a managed network in Resources a link: clicking it loads that stack on its node and opens the editor, reusing the existing fleet navigation. A latest-ref keeps the window listener current without re-subscribing each render. Image and volume badges are unchanged; only a managed network opts in via the new optional handler. * feat: add the Networking tab to the stack detail panel Add a capability-gated Networking tab that reads the per-stack networking facts and exposure intent. It shows the project networks (with external, internal, and created-by-stack flags), per-service network membership and aliases, published ports with their host-binding scope, network_mode and extra_hosts, and runtime drift, degrading to the declared model when the runtime is unavailable. Users can classify the stack and each service (internal, LAN, reverse proxy, public, and so on) or clear a row to inherit; the controls are read-only when the user cannot edit, and a broken exposure response never tears down the facts view. A new compose-networking capability is added to both registries so older nodes hide the tab, and the tab cross-links to the Doctor for the deploy and security findings. * docs: document the Compose Networking tab Add a feature page covering the Networking tab: the network facts, published ports and host bindings, the exposure-intent classification and inheritance, the exposure-aware Doctor findings, runtime drift, and a troubleshooting section. Register it in the docs navigation next to Compose Doctor. * feat: add a redacted network summary to the Stack Dossier export Append a network exposure section to the dossier Markdown: the stack and per-service exposure intents, the networks with their external and internal flags, and each service's published ports with their binding scope. It carries only names, intents, port numbers, and scope, never an env value or a label value. The summary is fetched only when the user exports (copy or download), so opening the panel costs nothing, and it degrades to omitting the section when the data is unavailable. The whole-fleet dossier export collects the same summary per stack, rethrowing the unauthorized sentinel like the sibling loaders. * feat: add a Fleet networking filter for exposure and drift Add a per-node networking summary that classifies a node's stacks as exposed (a host port published beyond loopback), unknown-exposure (publishes ports with no exposure intent set), or network-drift. It reads each stack's compose with the light dependency parser and one Docker snapshot, so it stays cheap across a node's full stack set, and it skips drift when the runtime is unreachable rather than inventing it. Serve it node-locally at GET /api/networking/summary, and aggregate it fleet-wide at GET /api/fleet/networking-summary: the hub computes its own summary in-process and reaches each remote through its node-local route, degrading an unreachable or older node to a skip. Because the aggregate lives under the proxy-exempt /api/fleet prefix it is never wrongly proxied. The Fleet overview gains a networking filter chip backed by that aggregate, fetched fail-soft and detached so it never gates the grid. * fix: spin the Networking refresh button while it reloads The refresh button silently refetched the same data, so a click gave no feedback. Track a refreshing state and spin the icon while the load is in flight, disabling the button, matching the Compose Doctor preflight button. * fix: apply effective per-service exposure intent to unclassified checks The "unclassified exposure" decisions only consulted the stack-level intent row, so a service classified directly (with no stack row) was still reported as unclassified, and a service explicitly marked unknown over a classified stack was missed. Both the exposure-unclassified preflight rule and the networking summary's unknown-exposure bucket now resolve the effective intent per publishing service (service row overrides stack row), matching the precedence already used by the exposure-internal-published rule. * fix: resolve drift network names via the compose top-level name When a compose file sets a top-level name:, Docker prefixes resource names with that project name instead of the stack directory. The light dependency parser dropped name:, so network-drift normalization compared runtime networks against directory-prefixed names and reported false network-undeclared / network-missing findings. Carry the parsed project name through DeclaredCompose and use it when normalizing declared networks for drift, while still filtering containers by the stack directory. |
||
|
|
38aabe7064 |
feat: health-gated updates and rollback readiness (#1354)
* feat: classify stack deploy and update failures with suggested next actions Failed deploy and update responses now carry a failure classification (cause category, headline, and suggested next step) derived from the compose error output. The recovery panel and chip render the classification and include it in copied diagnostics, and gateway-style failures surface as a node-unreachable cause. * feat: add update and rollback readiness reports for stacks Before a manual update, Sencho now shows an advisory readiness verdict computed from the stored preflight result, open drift findings, live container health, the pending image change, the rollback backup slot, and node disk headroom. The Stack Dossier gains a rollback readiness section that states what a rollback can restore and explicitly discloses that volume and bind-mounted data are not covered. Toolbar and sidebar updates now share one update path, and admins can create a fleet snapshot from the readiness dialog before updating. Nodes that do not advertise the capability keep the direct update flow. * feat: observe stack health after updates with a post-deploy health gate After a deploy or update succeeds, Sencho now watches the stack for a configurable observation window and records a passed, failed, or unknown verdict: containers must stay running, healthchecks must report healthy, and restart loops or disappearing containers fail the gate. The deploy panel shows the observation live and holds off auto-closing until the verdict lands, a failed gate surfaces the existing recovery actions including rollback, and the stack timeline records update started and gate verdict events. Scheduled, webhook, bulk, and git-source updates are gated the same way; rollbacks and installs are deliberately not. The gate is observational only and can be tuned or disabled per node under host alert settings. * docs: document health-gated updates and rollback readiness New operator page covering the update readiness dialog, the post-update health gate and its settings, the rollback readiness disclosure, and classified failures, with cross-links from the atomic deployments and deploy progress pages. The API reference gains the readiness and health-gate endpoints, the healthGateId success field, and the failure classification schema on deploy and update error responses. * feat: withhold the success verdict while the health gate observes An update used to show a green Succeeded that a failed health gate then contradicted moments later. The deploy modal now reports Verifying health while the gate observes, shows success only when the gate passes, and makes a failed or unknown gate the headline result; success toasts soften to a verifying message while a gate runs. The mobile recovery card groups its actions behind one bottom-right Take action menu so it stays compact on a phone, with the classified cause still visible on the card. A successful image update now also counts as the last known-good marker in rollback readiness, and the docs gain screenshots of the readiness dialog, gate states, dossier section, and settings. * fix: harden log format strings and the env existence path check Log calls that interpolated the stack name into the console format string now use constant format strings with placeholder arguments, and envExists validates path containment inline at its filesystem access, matching the established patterns used elsewhere in the same files. * test: adapt deploy modal success specs to the post-deploy health gate The deploy feedback modal now withholds its success verdict while the health gate observes the new containers, showing "Verifying health" until the gate passes. The two success-path E2E tests waited for "Succeeded" within the gate's 90s default window and timed out. Shorten the observation window to the 15s minimum for these tests via the settings API, assert the verify-then-succeed sequence the modal actually renders, and restore the default window afterward so the test value does not leak into later runs. * fix: serialize health gate polling and harden gate observation Address race conditions in the post-update health gate found in review. Backend: the gate poller used setInterval, so a Docker observe slower than the 5s tick could overlap the next poll and corrupt the restart and missing-container accounting, and a wedged socket could leave a poll pending forever. Polling is now single-flight: each cycle self-schedules the next only after it settles, and the observe is bounded by an 8s timeout so a hung probe counts as a poll error and resolves the gate unknown after three in a row. Frontend: the gate poller could overlap requests, letting a slow earlier "observing" response overwrite an already-applied terminal verdict. It is now single-flight with a terminal latch, so a late response can never roll the UI back from passed or failed. Also reject a non-digit nodeId on the snapshot coverage route instead of letting parseInt coerce it, document that turning off the deploy progress panel opts out of the live gate UI while the gate still runs server-side, and add gate-coverage tests for the webhook, git source, and auto-update apply paths plus the new single-flight, observe-timeout, and recovery cases. |
||
|
|
52ff0725f4 |
feat: add Compose Doctor preflight checks for stacks (#1348)
* feat: add Compose Doctor preflight checks for stacks Add an on-demand, advisory preflight that renders a stack's effective Compose model with `docker compose config` and runs a registry of deterministic checks before deploy, surfacing findings grouped by severity (blocker, high, warning, info) with a remediation for each. Findings cover unset env vars, host-port conflicts on the node, broad 0.0.0.0 exposure, missing bind-mount paths, a mounted Docker socket, privileged and host networking, moving image tags, missing restart policy and healthcheck, Swarm-only deploy fields, missing external networks or volumes, and container_name collisions. The report is node-scoped and stored as the last run per stack, and the route auto-proxies to the active node so a remote stack is checked on the node that owns it. A new Doctor tab on the stack detail panel runs preflight and shows the grouped findings, with a severity dot on the tab when the last run has blocker or high findings. The tab is gated on a compose-doctor capability so older nodes hide it. No environment value is ever stored, returned, or logged: only env key names and structural facts are read, and render failures surface a generic message or the missing required-variable names, never raw stderr. * fix: scroll the stack tab strip when its tabs overflow Adding the Doctor tab can push the per-stack Anatomy tab strip past the panel width on narrower layouts. Make the tab row scroll horizontally with subtle edge fades that appear only while there is more to scroll in that direction, so a panel wide enough to show every tab is unchanged. * fix: add clickable arrows and wheel scroll to the stack tab strip Hiding the scrollbar left mouse users with no way to scroll the overflowing tab row: a vertical wheel does not move a horizontal overflow and native rows do not drag-scroll. Replace the passive edge fades with clickable chevron arrows shown only when the row overflows that edge, and translate a vertical wheel over the row into horizontal scroll. * fix: inline the path-injection barrier in renderConfig CodeQL's path-injection check does not credit the wrapped isPathWithinBase helper as a sanitizer, so move the containment check inline at the spawn cwd sink, matching the canonical barrier used elsewhere in the codebase. Behavior is unchanged: the resolved stack directory must be contained in the compose base and may not be the base itself. * fix: hoist the compose-config spawn into the path-barrier scope The earlier inline barrier sat in a different scope than the spawn cwd sink (separated by the Promise-executor closure) and used a compound guard, so CodeQL did not credit it. Use the exact canonical startsWith barrier and hoist the spawn into the same scope as the check. Behavior is unchanged: the executor runs synchronously in the same tick as the spawn, so handlers still attach before any event can fire. |
||
|
|
d03d97d964 |
fix(nodes): close capability-gating gaps in node compatibility (#1261)
* fix(nodes): close capability-gating gaps in node compatibility Vulnerability scanning is now gated correctly on whether the active node advertises support for it: - A node without the Trivy binary stops advertising the scanning capability. Previously the capability was toggled only on a state change, so a node that booted without Trivy kept advertising scanning it could not perform. - The control node's own capability list now reflects features disabled at runtime, matching what it advertises to peers. - The scan history surface shows a clear "not available on this node" card, with its header actions hidden, instead of attempting a request that fails. A node's version and capability metadata now refreshes immediately after a connection test or a completed update, rather than waiting out the cache. Capability gates fail closed to the unavailable card when a node's metadata request errors, instead of staying open until the next fetch. Adds a test that fails if the frontend and backend capability lists drift, plus coverage for the metadata error path, the runtime-disabled local meta, the scanning capability sync, and the metadata cache invalidation paths. * fix(nodes): refresh node metadata client-side after a connection test A connection test dropped the server-side metadata cache, but the dashboard kept its own cached copy until the client TTL expired, so version and capability gates could stay stale in the browser. The test now forces a client-side metadata refresh for that node, so the version pill and gates reflect the node's current state immediately. Also strips any URL userinfo before logging the metadata fetch target, and makes the scanning-capability detection test deterministically exercise the no-binary disable path rather than depending on whether the runner has Trivy. |
||
|
|
0b50c88eb3 |
fix(fleet): route remote update trigger through getProxyTarget (#1123)
POST /api/fleet/nodes/:id/update and POST /api/fleet/update-all read node.api_url + node.api_token directly, so a pilot-agent row (which carries neither) returned "Remote node not configured." and was filtered out of bulk update. Both routes now resolve the target via NodeRegistry.getProxyTarget(), which returns the loopback URL for an active pilot tunnel and the configured api_url for proxy-mode remotes. fetchMetaForNode replaces the direct fetchRemoteMeta call for the self-update capability check. self-update is removed from PILOT_DISABLED_CAPABILITIES so a Compose-deployed pilot can advertise it; the host-console filter stays. SelfUpdateService still gates the local-end capability on the container actually carrying docker-compose labels, so a docker-run pilot self-disables and the Fleet UI sees a clean 503 instead of an ambiguous failure. Tests: new fleet-pilot-update covers the four single-node branches (success via loopback, null target, no self-update capability, meta offline) and two update-all cases (mixed-fleet dispatch, all-targets- null skip). capability-registry-pilot rewired to assert the new filter set. |
||
|
|
f6e42535c8 |
fix(mesh): route peer→central traffic over the existing forward WS (#1094)
* fix(mesh): route peer→central traffic over the existing forward WS The reverse mesh callback path (`/api/mesh/proxy-tunnel-from-peer`) needed SENCHO_PRIMARY_URL on central plus a publicly reachable origin from the peer's perspective. In a typical homelab where central sits behind NAT, peer→central dispatch silently failed at the dialer's short-circuit and the headline "call any service on any node by hostname" worked one way only. The forward WS at `/api/mesh/proxy-tunnel` is already bidirectional end to end. Make the bridge a persistent control-plane primitive: dial every mesh-enabled proxy peer at startup, reconcile every 60 s, never idle-close. Peer→central traffic multiplexes over the same WS via `tcp_open_reverse`. Removed: - `meshProxyTunnelFromPeer.ts` WS handler and dispatch - `MeshCentralRegistry`, `PeerToCentralMeshSessionDialer` - `mesh_handshake` first-frame state machine in `meshProxyTunnel.ts` - `maybeSendBootstrap`, `buildHandshakeFrame` in the dialer - `mesh_proxy_callback_bootstrap` capability and `maybeWarnUnsetPrimaryUrl` - `mesh_centrals` table (drop migration; greenfield, no users) - `PilotTunnelManager.replaceOrRegisterProxyBridge` (dead after handler removal) - twelve associated unit/integration tests plus the peer-recovery branch in `MeshService.openCrossNode` Added: - `MeshService.proactiveBridgeFanout` selects every mesh-enabled proxy peer (no longer gated on `mesh_stacks` rows) - `startBridgeReconcileLoop` runs the fanout every 60 s (override via `SENCHO_MESH_RECONCILE_INTERVAL_MS`) - `MeshProxyTunnelDialer` default idle TTL is now `0` and exposes `isDialing(nodeId)` for the status surface - `MeshNodeStatus.reverseCallbackStatus` discriminator (`connected | connecting | unavailable | not_applicable`) surfaced via `/api/mesh/status` and rendered as a pill in the Routing tab - `openCrossNode` error message distinguishes "no proxy target" from "waiting for central to dial the reverse bridge" - New tests: `mesh-service-proxy-tunnel-reconcile`, `mesh-status-reverse-callback`, `mesh-proxy-tunnel-dialer-no-idle-close` SENCHO_PRIMARY_URL is no longer required for any mesh function. * fix(mesh): rewrite proxy-tunnel reconcile test contents The previous commit renamed the file but the rewritten test bodies stayed unstaged on top of the rename. This commit lands the actual rewrite: the fanout assertion now requires every mesh-enabled proxy peer to be dialed, not just those with `mesh_stacks` rows, and adds a reconcile-tick repeated-call test. |
||
|
|
cf618dd866 |
feat(mesh): symmetric WS dial for proxy-mode mesh peers (#1066)
* chore(mesh): foundation for symmetric callback dial Adds the data-plane scaffolding that the symmetric callback dial fix builds on: - mesh_centrals table for peer-side bootstrap material - MeshCentralRegistry service (upsert/getActive/clear/markUsed/markRejected) - PilotTunnelManager kind discriminator and replaceOrRegisterProxyBridge - mesh_proxy_callback_bootstrap capability registration - MeshProxyTunnelDialer reason-tagged proxy-bridge-down events from a single tearDownBridge emission point - Reactive redial scheduler that skips idle and auth_failed reasons * feat(mesh): add reverse-direction activity log entries (closes R1-B) acceptReverseLocal now emits route.resolve.ok with direction=reverse on connect ack and route.resolve.fail with direction=reverse plus reason=container_not_found / connect_error pre-connect. Post-connect close/error stays silent. Reuses existing event types via the new details.direction discriminator so frontend filters are unaffected. * feat(mesh): add peer-to-central callback dial path (closes R1-A2) Closes the architectural gap where proxy-mode mesh peers could not re-establish their tunnel to central after any non-idle bridge teardown (idle close, network blip, central restart, peer reboot). Central remains the hub for the data plane; the change is purely about WS initiation. Symmetric WS initiation, asymmetric protocol roles. Central retains PilotTunnelBridge ownership; peer retains TcpStreamSwitchboard + reverseDialer ownership. Central bootstraps callback credentials over the first authenticated central-initiated mesh tunnel via a one-shot mesh_handshake JSON frame; peer persists the material in a new mesh_centrals SQLite table and dials central's new /api/mesh/proxy-tunnel-from-peer endpoint when local cross-node traffic needs a bridge and none is live. Mesh_tunnel JWT (HS256, signed with auth_jwt_secret) carries scope, audience, issuer (central instance id), peer api_token fingerprint, kid. Validation on inbound peer dial: algorithm pin, signature, scope, audience, instance, time bounds, node existence and mode, fingerprint match. Failures return HTTP 401 with a machine-readable reason; peer routes the response per a clear-vs-keep cache matrix. Triggers proactive bootstrap on mesh-enable and api_token rotation; central startup fans out to mesh-enabled proxy-mode nodes with mesh_stacks rows (throttled, fire-and-forget). Reactive redial on non-idle bridge loss. Capability-gated handshake send (mesh_proxy_callback_bootstrap) makes the upgrade path safe against older peers in mixed-version fleets. Adds peer-side /api/system/pilot-tunnels centralCallback diag block, bounded counter metrics for bootstrap and dial events. SENCHO_PRIMARY_URL preflight warning when unset on a central with mesh-enabled proxy nodes. Tested with unit suites for the validation chain, registry, manager, and both dialers; integration tests for bootstrap E2E (asserts protocol-role invariant), api_token rotation, instance id change, version skew, and pilot-mode regression. * fix(mesh): green CI on the symmetric callback branch Two independent CI failures, both surgical: 1. Backend tests (11 fails): four mesh test files called setupTestDb in beforeEach. setupTestDb does not reset the DatabaseService singleton, so the per-test afterEach rm of the previous tmpdir left the singleton connection pointing at a deleted file. The next beforeEach's line-55 write threw SQLITE_READONLY_DBMOVED on Linux. Windows file-lock semantics hid this locally. Hoist setupTestDb / cleanupTestDb to file-scope beforeAll / afterAll; per-test state resets stay in beforeEach. Matches the convention in the eight mesh test files that already pass. 2. CodeQL (4 high alerts): js/insufficient-password-hash flagged sha256(api_token) at four sites. The api_token is a 256-bit opaque bearer (sen_sk_-prefixed), not a human password; sha256 is the correct fingerprint primitive for binding the mesh_tunnel JWT to a specific token. Add the two production files plus the two test files that mint the fingerprint to the existing path-scoped query-filter for that rule. * fix(mesh): drop unused afterEach import and revert dead codeql config ESLint flagged afterEach as unused in mesh-central-registry.test.ts:1 after the previous commit hoisted setup/teardown to file-scope beforeAll/afterAll. Remove from the vitest import line. Revert the codeql-config.yml additions from the previous commit. The paths: sub-key under query-filters > exclude is not a documented CodeQL feature and silently no-ops. The four js/insufficient-password-hash alerts on api_token fingerprinting are tracked as dismissed false positives in the GitHub Security tab rather than via dead config. |
||
|
|
8dd0fce621 |
fix(fleet): show capabilities, version, metrics, and stacks for pilot-agent nodes (#1044)
When a pilot-agent node was the active node, the UI rendered "does not advertise this capability" across most tabs, a perpetual "Update available" badge, and a Fleet card body with blank CPU/RAM/Disk and "No stacks found". The cause was central-side aggregators in /api/fleet/* and /api/nodes/:id/meta only fanning out to proxy-mode remotes via node.api_url + node.api_token, which are null for pilot-agent. Route every affected aggregator through NodeRegistry.getProxyTarget so the loopback URL backed by the active pilot tunnel is used uniformly: - /api/nodes/:id/meta and /api/fleet/update-status fetch via the new NodeRegistry.fetchMetaForNode helper (resolves the target, delegates to fetchRemoteMeta, returns the shared OFFLINE_META on null). - fetchRemoteNodeOverview, /api/fleet/configuration, /api/fleet/node/:nodeId/stacks, and the stack-containers drilldown fetch through target.apiUrl with conditional Authorization. - fetchRemoteMeta omits the Authorization header when the token is empty (pilot-agent loopback) instead of sending a malformed Bearer string. - Pilot-agent rows preserve pilot_last_seen and mirror it into last_successful_contact so the Fleet "last seen" cell renders the recent tunnel timestamp during a brief reconnect. Pilot-mode capability filter excludes capabilities whose central-pilot path is not yet wired (host-console, self-update). Without this, the Console tab would surface for an Admiral pilot session and click through to central's host because the WS upgrade handler still gates on api_url + api_token. Filtered capabilities are removed at boot via applyPilotModeCapabilityFilter when SENCHO_MODE=pilot. Cache invalidation on tunnel-up: the meta cache for a reconnecting pilot is dropped so the next request rebuilds capabilities and version through the live bridge instead of waiting for the 3-minute TTL. The namespace constant moves to helpers/cacheInvalidation.ts alongside the new invalidateRemoteMetaCache helper. Husky commit-msg hook: add the missing shebang and a .gitattributes rule pinning .husky/* to LF line endings so commits do not fail with "Exec format error" on Windows shells where autocrlf=true converts the hook to CRLF. Tests cover Authorization-header behavior, pilot-mode filter idempotency, fetchMetaForNode dispatch (offline target, pilot-agent loopback, proxy-mode), and the four affected fleet routes for pilot-agent both when the tunnel is up and when it is down. |
||
|
|
dc8370f5a4 |
fix(security): harden Trivy scan lifecycle, logging, and docs (#639)
* fix(security): harden Trivy scan lifecycle, logging, and docs - Call TrivyService.initialize() at startup so capability state is accurate before first request; add periodic re-detect to the scheduler so newly installed Trivy binaries light up without a restart. - Add markStaleScansAsFailed sweep (+ idx_vuln_scans_status index) to recover any scan row left in_progress after a crash or timeout; sweep runs before the paid-tier gate so every tier self-heals. - Split scanImage persistence into beginScan/finishScan so the manual scan route owns a single code path and can return a scanId synchronously while work continues asynchronously. - Validate image refs on /api/security/scan and /sbom via new utility; defense-in-depth against shell-metacharacter payloads. - Dispatch a warning-level alert when a post-deploy scan fails so the operator has a user-visible path to the failure instead of a silent log. - Share DIGEST_CACHE_TTL_MS and severity ordering across service and route layers; remove dead invalidateDetection(). - Add [Trivy:diag] logging gated behind developer_mode for support diagnostics; production logs unchanged. - Frontend: defensive toast fallback chain, sr-only SheetDescription, and a truncation badge when the 500-item detail fetch is capped. - Tests: extend trivy-service and vulnerability-db suites; add image-ref and severity unit tests. - Docs: expand vulnerability-scanning troubleshooting with recovery, re-detect, and diagnostic-log guidance; link Dockerfile comment to trivy-setup. * fix(security): drop unnecessary escape in image-ref forbidden-char regex |
||
|
|
c9cd6990d2 |
feat(images): Trivy-powered vulnerability scanning (#635)
* feat(images): Trivy-powered vulnerability scanning Scan container images for known CVEs via Trivy. On-demand scanning and severity badges are available on every tier; scheduled scans, scan policies, SBOM generation, and scan history are gated to Skipper+. - New TrivyService (binary detection, per-image scan, SBOM, digest cache) - Three new tables: vulnerability_scans, vulnerability_details, scan_policies - 12 routes under /api/security (scan, results, summaries, SBOM, policies, compare) - Post-deploy async scans wired into all five deploy paths, with a per-deploy opt-out toggle in the App Store deploy sheet - "scan" action type added to SchedulerService for fleet-wide recurring scans - Frontend: severity badges in Resources Hub with animated cursor detail, scan results drawer with vulnerability table and filters, and a new Security section in Settings for scan policy CRUD - Policy threshold violations dispatch a warning or critical alert based on the policy's block_on_deploy flag; deploys themselves are never blocked * fix(security): compute scan age in useEffect to satisfy react-hooks/purity |
||
|
|
c4ff58347e |
fix(container-exec): harden with security fixes, validation, and test coverage (#577)
* fix(container-exec): harden with security fixes, validation, and test coverage - Enforce admin role at WebSocket upgrade for container exec sessions - Validate container is running before creating exec - Fix bash-to-sh fallback (move .start() inside try/catch) - Register container-exec as a capability for fleet visibility - Add standard and diagnostic logging for exec lifecycle - Fix false Admiral license claim in API docs - Fix design system violations in BashExecModal (hardcoded colors) - Remove duplicate legacy xterm dependencies - Add 18-test suite covering auth, validation, fallback, and cleanup * fix(container-exec): use correct Duplex type for exec stream The stream variable was typed as NodeJS.ReadWriteStream, which lacks .destroy(). Dockerode's Exec.start() returns stream.Duplex per its type definitions. This caused tsc to fail while Vitest (which skips full type checking) passed. |
||
|
|
8080540881 |
fix(fleet): strip trailing slash in fetchRemoteMeta URL construction (#444)
Nodes with a trailing slash in api_url (e.g. http://host:3000/) caused fetchRemoteMeta to construct a double-slash URL (http://host:3000//api/meta), which failed silently. The update-status endpoint then returned version: null for the remote node, triggering a false "Update available" badge. Every other URL construction in the codebase already strips the trailing slash; this was the one call site that was missed. |
||
|
|
8ba4532995 |
fix(fleet): resolve version detection using package.json over stale generated constant (#410)
* fix(fleet): resolve stuck update states and improve update UX The fleet node update flow had several bugs: the in-memory update tracker never cleared terminal states (timeout, failed, completed), leaving nodes permanently stuck with no way to retry or dismiss. The Recheck button only re-fetched stale state without clearing it, and the POST trigger rejected retries with 409 even after timeout. Backend fixes: - Add DELETE endpoints (single node + batch) to clear tracker entries - Fix 409 race: detect expired timeouts and clear terminal states before re-triggering - Populate error messages in the tracker for timeouts and failures - Include error field in the update-status API response - Auto-expire completed entries after 60 seconds Frontend fixes: - Add retry (RotateCcw) and dismiss (X) buttons on failed/timed-out badges - Show error details via animated cursor hover (CursorFollow pattern) - Recheck button now batch-clears all terminal states before fetching - Recheck shows loading spinner and disables while checking - Extract NodeCardProps interface for readability * fix(fleet): detect update completion via process start time Remote nodes that cannot report their version (e.g. older builds) caused updates to always time out because completion detection relied solely on version comparison. The gateway now tracks the remote node's process start time from /api/meta and detects container restarts by comparing it across polls. Also extracts a createTracker() factory to eliminate repeated object construction across 5 call sites. * docs: add troubleshooting for first-update timeout on old nodes Adds a new troubleshooting entry explaining why the first remote update on nodes running pre-v0.40.0 always times out (neither version nor process start time can be detected). Documents the fix: dismiss, recheck, and confirm the node updated. Also adds a screenshot of the timed-out state with retry/dismiss buttons to the remote updates feature page. * fix(fleet): detect update completion via offline detection and error reporting The update completion detection relied on version change and process start time, both of which fail on nodes running older Sencho versions that report "unknown" and lack the startedAt field. This caused every update to time out after 5 minutes. Add three-signal detection: version change, process restart (startedAt), and offline/online detection (node went unreachable during update and came back). Also add a 90-second early failure heuristic for when the remote image pull fails silently, and surface pull errors from SelfUpdateService via /api/meta so the gateway can report them immediately. * fix(deps): bump vite to 8.0.5 to resolve high severity vulnerabilities Fixes GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583. * fix(deps): bump vite in backend lockfile to resolve audit failures Vitest pulls in vite as a transitive dependency. Bumps to 8.0.5. * fix(fleet): resolve version detection using package.json over stale generated constant resolveVersion() previously returned the build-time SENCHO_VERSION constant without checking the root package.json. When a branch fell behind a release-please version bump, the generated constant was stale, causing remote nodes to show "unknown" version and false "Update available" badges. The function now walks up to the root package.json first (authoritative source) and falls back to the generated constant only if the walk fails. |
||
|
|
cc2da99d6f |
fix(fleet): resolve stuck update states and improve detection (#405)
* fix(fleet): resolve stuck update states and improve update UX The fleet node update flow had several bugs: the in-memory update tracker never cleared terminal states (timeout, failed, completed), leaving nodes permanently stuck with no way to retry or dismiss. The Recheck button only re-fetched stale state without clearing it, and the POST trigger rejected retries with 409 even after timeout. Backend fixes: - Add DELETE endpoints (single node + batch) to clear tracker entries - Fix 409 race: detect expired timeouts and clear terminal states before re-triggering - Populate error messages in the tracker for timeouts and failures - Include error field in the update-status API response - Auto-expire completed entries after 60 seconds Frontend fixes: - Add retry (RotateCcw) and dismiss (X) buttons on failed/timed-out badges - Show error details via animated cursor hover (CursorFollow pattern) - Recheck button now batch-clears all terminal states before fetching - Recheck shows loading spinner and disables while checking - Extract NodeCardProps interface for readability * fix(fleet): detect update completion via process start time Remote nodes that cannot report their version (e.g. older builds) caused updates to always time out because completion detection relied solely on version comparison. The gateway now tracks the remote node's process start time from /api/meta and detects container restarts by comparing it across polls. Also extracts a createTracker() factory to eliminate repeated object construction across 5 call sites. * docs: add troubleshooting for first-update timeout on old nodes Adds a new troubleshooting entry explaining why the first remote update on nodes running pre-v0.40.0 always times out (neither version nor process start time can be detected). Documents the fix: dismiss, recheck, and confirm the node updated. Also adds a screenshot of the timed-out state with retry/dismiss buttons to the remote updates feature page. * fix(fleet): detect update completion via offline detection and error reporting The update completion detection relied on version change and process start time, both of which fail on nodes running older Sencho versions that report "unknown" and lack the startedAt field. This caused every update to time out after 5 minutes. Add three-signal detection: version change, process restart (startedAt), and offline/online detection (node went unreachable during update and came back). Also add a 90-second early failure heuristic for when the remote image pull fails silently, and surface pull errors from SelfUpdateService via /api/meta so the gateway can report them immediately. * fix(deps): bump vite to 8.0.5 to resolve high severity vulnerabilities Fixes GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583. * fix(deps): bump vite in backend lockfile to resolve audit failures Vitest pulls in vite as a transitive dependency. Bumps to 8.0.5. |
||
|
|
a55d1245f8 |
fix(fleet): resolve version detection pipeline for Docker builds (#402)
* fix(fleet): resolve version detection pipeline for Docker builds The Dockerfile backend-builder stage was missing a COPY of the root package.json, causing generate-version.js to fall back to "0.0.0-dev" at build time. At runtime, the filesystem walk also failed (root package.json not in the final image), producing the string "unknown" which the frontend rendered as "vunknown". Changes: - Dockerfile: copy root package.json into backend-builder stage - CapabilityRegistry: return null (not "unknown") for unresolvable versions; add isValidVersion() type guard; normalize remote meta responses to strip "unknown"/"0.0.0-dev" sentinel values - Fleet endpoints: hoist gateway version validation outside per-node loops; treat unresolvable remote versions as "potentially outdated" instead of silently marking them up to date - FleetView: guard all version display points (card badge, update button, gateway label, modal columns) via shared formatVersion() - EditorLayout, CapabilityGate: use shared isValidVersion utility - New frontend/src/lib/version.ts shared utility - Docs: add troubleshooting section for version display edge cases - Screenshots: updated Fleet Overview and Node Updates modal * docs: update fleet node updates screenshot with live remote node |
||
|
|
670a429168 |
fix(fleet): resolve getSenchoVersion crash in Docker containers (#396)
* fix(fleet): resolve getSenchoVersion crash in Docker containers Replace the runtime __dirname walk (which fails in Docker because the root package.json is absent from the final image) with a build-time version injection. A prebuild script reads the root package.json and generates src/generated/version.ts with the version baked in as a constant. The compiled output carries the correct version regardless of the container's filesystem layout. Retains a filesystem walk fallback for dev environments where the prebuild hook may not have run. * fix(fleet): skip postinstall version generation when script is absent The Docker prod-deps stage copies only package.json (no source files) before running npm ci --omit=dev. The postinstall hook now checks for the script file before executing, so it no-ops in stages where scripts/generate-version.js has not been copied. |
||
|
|
d437a195b6 |
fix(fleet): resolve getSenchoVersion crash in Docker containers (#391)
getSenchoVersion() used require('../../../package.json') which resolves
correctly in dev (src/services/ -> 3 up -> root) but fails in Docker
where the compiled file is at dist/services/ (2 levels from /app/).
The 500 error on /api/meta caused fetchRemoteMeta to always return
empty capabilities, blocking all capability-gated features on remote
nodes. Replace with a __dirname-based walk that finds the root
package.json (name === 'sencho') regardless of directory depth.
|
||
|
|
9e0c9d3f2d |
fix(licensing): backward-compatible tier/variant enforcement and self-healing variant detection (#385)
Accept legacy tier ('pro') and variant ('personal', 'team') names from older
remote nodes, normalizing them to current values ('paid', 'skipper', 'admiral')
in authMiddleware. This fixes distributed license enforcement failing between
v0.38.3 and v0.38.0 nodes due to the tier rename in v0.38.1.
Also fixes:
- Self-healing getVariant() that cross-checks stored variant_type against
product/variant name metadata on every call, correcting stale cached values
from previous buggy resolution logic
- Unguarded API responses in ResourcesView causing potential t.map crashes
- Fleet update status now polls on a 120s interval (was only fetched on mount)
- fetchRemoteMeta failures now logged for diagnosability
|
||
|
|
87b5908288 |
feat(fleet): add remote node update management (#353)
Add the ability to check for outdated nodes and trigger over-the-air updates from Fleet View. Nodes self-update by pulling the latest Docker image and recreating their container via the "last breath" pattern. Backend: - SelfUpdateService: self-container identification via HOSTNAME + Docker Compose labels, triggers pull + force-recreate - CapabilityRegistry: runtime capability disabling via disableCapability() - POST /api/system/update (202 + deferred self-update) - GET /api/fleet/update-status (version comparison across fleet) - POST /api/fleet/nodes/:nodeId/update (single node) - POST /api/fleet/update-all (bulk remote update) - In-memory update tracker with 5-min timeout Frontend: - Node Updates modal with summary stats, search filter, table layout, per-node Update buttons, and bulk Update All - Version badges and update-available indicators on node cards - ReconnectingOverlay for local node updates (polls /api/health) - 5s fast-poll when any node is actively updating - UpdateStatusBadge shared component for consistent badge rendering Requires Skipper (Pro) tier. Nodes must be deployed via Docker Compose with Docker socket access. |
||
|
|
ee75811e25 |
feat(nodes): add capability-based node compatibility negotiation (#350)
* feat(nodes): add capability-based node compatibility negotiation Each Sencho instance now exposes /api/meta with its version and supported capabilities. When the user switches nodes, the frontend fetches this metadata and disables features the remote node doesn't support via a CapabilityGate overlay. Version is shown in the node switcher dropdown and connection test results. - Backend: CapabilityRegistry with static capability list and fetchRemoteMeta helper - Backend: /api/meta (public) and /api/nodes/:id/meta (auth) endpoints - Frontend: NodeContext enhanced with per-node meta caching (5min TTL) - Frontend: CapabilityGate component with typed Capability union - Frontend: 13 features wrapped with capability gates - Docs: node-compatibility.mdx + OpenAPI spec updates * fix(nodes): revert to require() for package.json version reading The static import fails in the Docker multi-stage build because the root package.json is not copied into the backend-builder stage. The require() call resolves at runtime when the file is available. |