mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
48cebf9501e2444b03e8b6ec6f16545ef30a8fdf
1499 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
48cebf9501 |
fix: bind deploy progress, request, and health gate to the captured node (#1357)
* fix: bind deploy progress, request, and health gate to the captured node A deploy/update/install/git-apply re-read the active node from localStorage independently at three points: the progress WebSocket at mount, the POST at call time, and the health-gate poll. If the active node changed between the click and any of those, the operation, its live output, and its health verdict could target different nodes, and the socket and POST splitting across nodes broke output streaming. Capture the operation's node once when it starts and thread it through every leg. A new nodeId option on apiFetch overrides the active-node read, the progress terminal takes a nodeId prop for its socket URL, the health gate polls on the captured node, and a failed gate records its recovery entry only on the node it ran on. The surface, the request, and the gate now always agree. * fix: scope failed-gate recovery to the file list's node and harden node targeting Addresses review findings on the captured-node binding: - Track the node the stack file list was fetched for (filesNodeId) and record a failed gate's recovery entry only when it matches the gate's node. This closes a race where switching back to the gate's node could match a same-named stack from the previous node's still-loaded list before the new list lands, keying the record to the wrong file and blocking the correct one. refreshStacks now carries a sequence token so an out-of-order resolution cannot leave files and filesNodeId inconsistent. - Make an explicit apiFetch nodeId authoritative over a caller-supplied x-node-id header. - Add the missing stack-logs nodeId cases (null, and active-node fallback) to the terminal tests. |
||
|
|
a2fe58f62f |
chore(deps): bump @grpc/grpc-js (#1356)
Bumps the all-npm-backend-security group with 1 update in the /backend directory: [@grpc/grpc-js](https://github.com/grpc/grpc-node). Updates `@grpc/grpc-js` from 1.14.3 to 1.14.4 - [Release notes](https://github.com/grpc/grpc-node/releases) - [Commits](https://github.com/grpc/grpc-node/compare/@grpc/grpc-js@1.14.3...@grpc/grpc-js@1.14.4) --- updated-dependencies: - dependency-name: "@grpc/grpc-js" dependency-version: 1.14.4 dependency-type: indirect dependency-group: all-npm-backend-security ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
e20f1fe415 |
feat: add an inline deploy-progress style for the stack detail (#1355)
* feat: add an inline deploy-progress style for the stack detail Deploy progress gains a presentation choice under Settings > Appearance > Display: Modal (the default centered overlay) or Inline. In Inline style a compact status band on the stack detail shows the running operation, its elapsed time, the live phase, the latest output line, and the post-update health gate result. A "View output" button opens the full log modal on demand, a dismiss control clears the band, and the band auto-clears a few seconds after a clean completion. The live progress socket is lifted to an always-mounted owner so the band streams without the modal; the default Modal style is unchanged. Operations carry their node so a band never bleeds onto a same-named stack on another node. The stack detail's redundant "CONTAINERS" section heading is removed; the band reserves that vertical space. * fix: keep inline deploy progress reachable off the stack detail Review of the inline presentation found a gap: a failed operation, an App Store install, or navigating away leaves the inline session with no visible surface, since the band only renders on the operation's own stack detail. Restore the minimized pill as the inline fallback, shown only when the band is not covering the session, so there is always a click-through to the log without ever overlapping the band. Closing the modal for a failed op now ends the session (the band has stepped aside) instead of only hiding it. Also document the unsupported mid-operation style switch, and refresh the deploy-progress, settings, appearance, and app-store docs for the renamed "Deploy progress" setting and the Modal/Inline choice. |
||
|
|
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. |
||
|
|
739bbf990e |
feat: flag documentation drift in the stack dossier (#1349)
* feat: flag documentation drift in the stack dossier Warn in the Dossier tab when a port written into a stack's access_urls is not published by the stack's compose, so operator documentation stays aligned with what Sencho can observe. The check is deterministic, read-only, and frontend-only: it compares ports parsed from access_urls against the published ports the Anatomy panel already shows, never interprets prose, and stays quiet for port-less URLs, scheme-default ports (:80/:443), and ports published through a variable, to avoid false positives. Community tier, no gating. * test: pin doc-drift handling of bare hosts and mixed variable ports Make two deterministic-drift behaviors intentional and regression-proof after review: a scheme-less single-label host (plex:32400) is not checked, since it cannot be told apart from a plain note (add a scheme to opt in), and a stack mixing a variable-published port with fixed ports suppresses the whole check. Adds tests for both, a clarifying code comment, and a docs note with the http:// workaround. No behavior change. |
||
|
|
c2ac15b06a |
chore(deps): bump github/codeql-action (#1352)
Bumps the all-actions group with 1 update in the / directory: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 4.36.1 to 4.36.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/87557b9c84dde89fdd9b10e88954ac2f4248e463...8aad20d150bbac5944a9f9d289da16a4b0d87c1e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.36.2 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> Co-authored-by: Anso <dev@saelix.com> |
||
|
|
60d138ac33 |
chore: raise dev auth rate limit so the full E2E suite is not blocked (#1353)
The Playwright E2E suite logs in per test (fresh context each) and has grown past the 100-attempt/15-min dev cap on the auth limiter, so the last test in a run gets 'Too many attempts' and fails. Raise the non-production cap to 1000 to restore headroom for the suite and local tooling. Production stays at 5 attempts/15min, so brute-force protection is unchanged. |
||
|
|
c0b83d8326 |
chore(deps): bump the all-npm-backend group in /backend with 6 updates (#1351)
--- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: semver dependency-version: 7.8.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: "@types/node" dependency-version: 25.9.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: typescript-eslint dependency-version: 8.61.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: "@aws-sdk/client-ecr" dependency-version: 3.1065.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: "@aws-sdk/client-s3" dependency-version: 3.1065.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
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. |
||
|
|
d369b03a38 |
feat: detect stalled stack updates and add in-app recovery actions (#1347)
* feat: detect stalled stack updates and add in-app recovery actions Add a backend idle-output backstop that stops a deploy/update compose step that has gone silent (SENCHO_COMPOSE_STALL_TIMEOUT_MS, default 10m), so a hung image pull surfaces a fast failure instead of spinning indefinitely. Surface failed, timed-out, and stalled operations with recovery actions on the stack page: a desktop chip plus popover menu and an inline mobile card offering retry, restart, roll back (when a backup exists), refresh state, and copy diagnostics, all gated by deploy permission. The streaming deploy/update progress modal is now on by default and warns when output goes quiet. Container state is refreshed after a failed or stalled operation, and the UI never sits in an indefinite spinner. * fix: harden rollback against policy-blocked file mutation and refine recovery Address review findings on the stalled-update recovery work: - The rollback route restored backup files before running the policy gate, so a policy-blocked rollback could leave the on-disk config rolled back while the deployed containers were unchanged. Snapshot the current files first and revert them when the gate blocks; if that revert itself fails, escalate it on the persistent alert feed since the 409 is already sent. - Refresh container state after a successful manual rollback (rollback redeploys), without mis-recording a refetch failure as a rollback failure. - Suppress the stalled-output warning once live progress is unavailable. * test: mock snapshotStackFiles in the atomic-deploy rollback route tests The rollback route now snapshots stack files before restoring a backup, so its FileSystemService mock needs snapshotStackFiles. Without it the mocked call threw and the route returned 500, failing the success-path rollback assertions. |
||
|
|
a3033a848e |
ci: scope CodeQL e2e tmpfile suppression via paths-ignore (#1346)
The js/insecure-temporary-file rule fires on e2e Playwright specs that seed fixtures into the backend's COMPOSE_DIR (a fixed /tmp path) so the API under test can read them back. A randomized mkdtemp cannot apply there: the backend resolves paths against its own COMPOSE_DIR, so a fixture written elsewhere would be invisible to it. The prior suppression used a paths key inside a query-filters exclude, which CodeQL ignores: query-filters match on query metadata, not source path. That left the rule firing on every new e2e spec. Move the exclusion to a top-level paths-ignore, the only mechanism that scopes analysis by source path, so the e2e specs stop tripping the rule. |
||
|
|
fd10c49ee4 |
chore(main): release 0.91.1 (#1344)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com> Co-authored-by: Anso <dev@saelix.com>v0.91.1 |
||
|
|
05e7db58e4 |
chore(deps): bump frontend dependencies (#1345)
Apply the queued frontend dependency group update (29 packages, notably @xyflow/react 12.11.0, radix-ui minors, react 19.2.7, vite 8.0.16, and vitest 4.1.8) and regenerate the lockfile. @xyflow/react 12.11.0 retyped OnNodeDrag so the drag event is the DOM MouseEvent | TouchEvent rather than a React synthetic event. Update the FleetTopology onNodeDragStop handler signature to match. |
||
|
|
e7895c889d |
fix: base Stack health uptime on container start, not creation (#1341)
The dashboard Stack health UP column counted from each container's Created timestamp, which never moves on stop/start or restart, so a restarted container kept reporting its original age. Resolve uptime from State.StartedAt (via a briefly cached inspect with bounded concurrency, falling back to Created when inspect is unavailable) so it reflects the real time since last start. The current CPU and MEM columns separately summed the latest sample per container with no recency filter, letting a recently stopped container's final reading linger in the totals. Drop samples that trail the freshest sample by more than the stale window so stopped containers leave the sum. |
||
|
|
3dc8199907 |
ci: publish a dev integration image on every push to main (#1343)
Add docker-dev.yml: on each push to main (and manual dispatch) it builds the multi-arch image, runs the same Trivy and smoke-test gates as the release build, and pushes ghcr.io/studio-saelix/sencho-dev with the tags :dev (moving) and :dev-<short-sha> (immutable), so a maintainer can pull and test the exact artifact queued for the next release before it can become a public release. The integration path is GHCR-only and non-promotable: no Docker Hub, no latest/semver tags, no cosign signing, no SBOM/VEX, and no GitHub Release. All of that stays release-only in docker-publish.yml, driven by v* tags. Least-privilege permissions (contents:read, packages:write) and no production environment, so dev images publish automatically with no Docker Hub credentials in scope. Also add a clarifying header comment to docker-publish.yml noting it is the release-only path. |
||
|
|
8e4a09f44a |
chore: drop stale prototype path from mobile-ui comment (#1342)
The mobile primitives comment pointed at a design prototype folder that no longer exists. Remove the dead path reference; the comment still describes where the primitives came from. |
||
|
|
4a619e69f4 |
chore(main): release 0.91.0 (#1325)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com> Co-authored-by: Anso <dev@saelix.com>v0.91.0 |
||
|
|
45c004e1cd |
docs: reflect Community tier rebalance for scanning, audit log, and registries (#1340)
Update tier mentions for the capabilities now available on Community: - Trivy managed auto-update, single-scan SBOM export, node labels, and the fleet topology layout modes are present-tense Community features. - The audit log documents a 14-day recent-activity window on every tier, with CSV/JSON export, anomaly detection, and configurable retention as Admiral. - Private registries split Docker Hub / GHCR / custom (every tier) from AWS ECR (Admiral). SARIF export and deploy enforcement stay Admiral. Also removes two fence-spec phrasings (per-tier label cell and 'visible on Admiral' for the audit tab) in favor of stating each requirement once. |
||
|
|
2298f470bd |
feat: allow local Docker Hub, GHCR, and custom registry credentials on Community (#1338)
Private registry credentials are no longer paid-only. Community admins can add and manage Docker Hub, GHCR, and custom/self-hosted registry credentials, stored locally on the node. AWS ECR (short-lived token refresh, AWS region) stays on the paid tier. Backend gates ECR per-type via a single helper applied at create, update (using the effective type so an existing row cannot be switched to ECR), the per-id connection test (gated on the stored type), and the stateless test. The admin role and API-token-scope rejection are unchanged on every route. The frontend drops the blanket paywall on the Registries section, filters ECR out of the type selector for Community, and states the ECR requirement inline. |
||
|
|
ea1267b32f |
feat: give Community a 14-day in-app audit log (#1337)
* feat: give Community a 14-day in-app audit log The audit-log list view is now reachable on the Community tier, scoped to a rolling 14-day recent-activity window, with the existing filters (actor, method, search, date range). Full retention, CSV/JSON export, and per-row anomaly annotation remain on the paid tier. Backend clamps the list window for unpaid tiers and forces anomaly annotation off; a non-numeric from query value is normalized so it cannot lift the clamp. The stats and export endpoints keep their paid gate. The frontend hides the export control and the anomaly stat tiles for Community and omits the anomaly request. Audit entries are still captured for every tier, so only read access changes. * test: repoint distributed-license stand-in to a still-paid audit route The distributed-license trust-chain test borrowed GET /api/audit-log as a paid-gated, DB-reading stand-in. That route is now Community-accessible, so its six 403 PAID_REQUIRED assertions flipped to 200. Point the stand-in at GET /api/audit-log/stats, which keeps requirePaid plus the system:audit permission gate and is satisfied by both token types the test uses. |
||
|
|
54119be0c2 |
feat: move trivy auto-update, node labels, fleet topology, and single-scan SBOM to Community (#1336)
Rebalance several capabilities from the paid tier to the free Community tier: - Managed Trivy auto-update toggle is admin-only, no longer tier-gated. - Node labels (assign, view, manage) are available on every tier; cordon and FleetSync anchor reset stay paid. - Fleet topology layout modes (Hub, Grouped, Free) are available on Community. - Single-scan SBOM export (SPDX and CycloneDX) is admin-only on Community; SARIF export stays paid via a dedicated canExportSarif capability split out from the former shared SBOM flag. Backend route guards and frontend affordances are updated together, with tier and admin-role tests covering the Community-allowed and still-paid paths. |
||
|
|
710647a44f |
feat(snapshots): preserve stack dossiers with fleet snapshots (#1339)
* feat(snapshots): preserve stack dossiers with fleet snapshots Fleet snapshots can now optionally capture each stack's Dossier notes alongside its compose and .env files, so a recovery restores the operational knowledge around a stack, not just its configuration. - Opt-in global setting "snapshot_documentation" (default off), toggled from the renamed Fleet settings section. - Capture reads local dossiers from the database and remote dossiers over the Distributed API proxy; only stacks with notes are recorded, and secret values are never included. - Captured notes are stored encrypted at rest in a new fleet_snapshots column and surfaced in the snapshot detail view behind a badge. - Cloud and downloaded archives gain a documentation.json (archive_version 2). - Restore stays conservative: dossier notes are written back only when the operator explicitly opts in, on both single-stack and restore-all paths. - Existing snapshots and archives remain valid; behavior is unchanged when the setting is off. * fix(snapshots): harden dossier-notes restore against bad input and partial failures Address review findings on the documentation-snapshots restore path: - Parse `restoreNotes` strictly (=== true) on single-stack restore, matching restore-all, so a stray non-boolean can never opt in to overwriting notes. - Guard findSnapshotDossier: require an array of stacks and real dossier content, so a malformed or all-blank entry can't clobber current notes. - Make the dossier-notes write non-fatal relative to the file restore: a notes failure (e.g. a remote dossier PUT) is caught, reported via `notesError`, and no longer 500s the single restore or fails the stack in restore-all once the files are already written. - Surface the partial outcome in the UI: a warning toast on single restore, a summary note on restore-all, and gate the "Documentation captured" badge and restore-all notes control on captured stacks while rendering capture warnings. Adds tests for strict parsing, malformed/blank blobs, remote notes restore (success + non-fatal failure, single and bulk), and scheduled capture-on. * fix(snapshots): drop unused binding in restore-all remote notes test The restore-all remote notes test destructured a node id it never uses (restore-all is driven by snapshot id alone), tripping no-unused-vars and failing the lint step. Bind only the snapshot id. |
||
|
|
842ee7dd0c |
feat(fleet): export a whole-fleet Markdown dossier (#1334)
* feat(fleet): export a whole-fleet Markdown dossier Add an admin-only "Export Dossier" action to the Fleet view that walks every node and stack, pairs each stack's generated Compose anatomy with its operator notes, and downloads a folder-structured homelab-dossier.zip (index, per-node and per-stack pages, plus fleet-wide port, volume, network, env, access-URL, and VLAN/firewall maps). Reuses the existing stack dossier and anatomy Markdown generators by extracting the shared Compose parsers into a frontend lib module. Unreachable nodes are recorded with a reason and never block the export; only env variable names and counts are ever emitted, never values. * fix(fleet): unique stack slugs and reproducible dossier archive Disambiguate stack names on one node that slugify to the same value (e.g. `Web` and `web` on a case-sensitive host) with a per-node slug map shared by the node-page links and the file emission, so neither overwrites the other. Pin a fixed entry timestamp on the zip so the archive bytes are a pure function of the file map rather than the wall clock. |
||
|
|
b21324f97a |
feat(stacks): persist a drift ledger with temporal source-change detection (#1333)
* feat(stacks): persist a drift ledger with temporal source-change detection Build on the read-only compose-vs-runtime drift check so a stack's drift is remembered over time, not just shown at a glance. - Record a deploy baseline: on a successful deploy, update, or rollback, store the deployed compose file's source and rendered-model hashes on the stack so the Drift tab can tell whether the file has changed since the last deploy. - Surface temporal drift in the Drift tab: "matches last deploy", "source changed since last deploy" (distinguishing a model change from a formatting-only edit), or "no deploy baseline yet". - Persist findings into a drift ledger: a re-check reconciles the current findings, recording newly detected ones and resolving cleared ones, and shows a short drift history under the findings. The drift report read stays side-effect-free; only an explicit re-check (and a deploy) writes the ledger. - Write drift detected/resolved events to the stack Activity timeline so the provenance sits alongside deploys and restarts. Node-local and available on the Community tier. Reconciliation is skipped when a check is not authoritative (Docker unreachable or a compose parse error) so an open finding is never falsely cleared. * fix(stacks): record the drift baseline for every deploy path and harden the ledger Address review feedback on the drift ledger: - Record the deploy baseline in ComposeService.deployStack/updateStack instead of only the manual route, so bulk, Git-source, App Store, scheduler, and webhook deploys all capture source/rendered hashes. Reconciliation stays on the explicit re-check. - Store no rendered baseline when the local parser cannot model the compose (for example a file over the parse cap) rather than a sentinel that would make a later real change read as unchanged. - Let temporal-overlay failures surface as a 500 instead of being hidden behind a neutral "no baseline"; only the compose read stays best-effort. - Omit the temporal card entirely when a report (for example from an older remote node) carries no temporal data, instead of showing a misleading "no baseline". - Keep drift_detected / drift_resolved history-only by excluding them from the routable-category whitelist, so they are never offered as a channel route that would never fire. - Use a JSON separator for the finding identity key so the source file is plain text (no embedded control byte). * fix(stacks): sanitize logged errors in the drift report handlers The drift report and re-check handlers logged the caught error object raw alongside the stack name, which a code scan flagged as a log-injection vector: a crafted stack name surfacing inside an error message or stack could forge log lines. Route the error through the log sanitizer so control characters are stripped before writing. Render it with util.inspect first so the stack trace, cause chain, and underlying error codes are preserved for debugging. |
||
|
|
421177e4a6 |
feat(stacks): add compose-vs-runtime drift detection (#1329)
* feat(stacks): add compose-vs-runtime drift engine Add a read-only engine that compares a stack's on-disk compose model against the live Docker runtime and reports where the two diverge. GET /api/stacks/:stackName/drift returns a per-stack report with a status (in-sync, drifted, missing-runtime, unreachable) and typed, service-scoped findings: a declared service with no running container, a running container not declared in compose, an image mismatch, and a published-port mismatch. The report is computed at request time with no persistence and is available on every tier. The check reuses the existing compose parser and Docker dependency snapshot; the compose parser now also captures each service's declared image. Boundaries fail closed: an unreadable compose file reports drifted and an unreachable Docker daemon reports unreachable, never a false in-sync. * fix(stacks): keep drift hasContainers accurate on compose parse error assembleStackDrift hardcoded hasContainers: false on the parse-error path, contradicting the field's contract when the runtime actually has running containers. Compute it once from the container set and reuse it across all return paths. Also add a route test that exercises the successful 200 path for an existing stack on the Community tier (stubbing only the Docker boundary), so a tier gate or handler regression after the existence check is caught. * feat(stacks): add a drift detection tab to the stack view Surface the compose-vs-runtime drift report on the per-stack Anatomy panel as a read-only Drift tab. It shows the stack's status (in sync, drifted, not running, unreachable) and, when drifted, the specific service-scoped reasons with the declared and running values side by side. A re-check action reruns the comparison. The tab lives in the shared anatomy panel, so it appears on both the desktop stack view and the mobile stack detail. Available on every tier. * fix(stacks): sanitize the logged error in the drift report builder The compose-read and Docker-snapshot catch blocks logged the raw error object, whose message can embed the user-controlled stack path (e.g. an ENOENT path). Log the error through the existing sanitizer so a crafted stack name cannot forge log lines, matching the pattern used elsewhere in the stacks router. |
||
|
|
8302048bc4 |
test(mobile): declare per-view mobile treatment and harden the visual gate (#1332)
* test(mobile): single-source map for per-view mobile treatment Declare how every top-level view behaves on a phone in one place: MOBILE_TREATMENTS is a Record<ActiveView, ...>, so adding a new view without classifying it (bespoke / responsive / desktop-only / detail) fails the type check. BESPOKE_MOBILE_VIEWS is derived from it instead of hand-maintained, and a unit test keeps the two in lockstep and pins the current bespoke set so a change is deliberate. EditorLayout consumes the derived set; behavior is unchanged. * ci(visual): run the desktop-unchanged gate in its own job Split the visual-regression spec into a dedicated Playwright "visual" project, excluded from the default chromium project the functional E2E job runs, so a missing or platform-mismatched baseline can no longer fail every PR. Add a Visual Regression workflow: a compare job gates PRs into main against committed baselines (and skips with a warning until they are seeded), and a manual seed job regenerates the baselines on the Linux runner and commits them to a feature branch (refusing main). Baselines are platform-specific, so they must be produced on the runner rather than locally. Drop the stack-detail view from the gate: a fresh CI app has no stack to open and its live log stream is not deterministic; the shell plus the four content views still catch a desktop base-class regression. |
||
|
|
928a3a8343 |
feat(mobile): bespoke phone layouts for dashboard, fleet, schedules, and settings (#1330)
* feat(mobile): masthead-led dashboard and 5-tab bottom nav on phones On phones (below the md breakpoint) the dashboard now renders a bespoke, masthead-led layout instead of the reflowed desktop workspace: - A status masthead leads with the overall system-health verdict, the node, and a live summary (stack counts, last sync, a "metrics stale" marker when polling stops). - A CPU hero card with a sparkline, then a memory / disk / network strip with threshold-colored bars, then a tappable stack-health list. - The bottom tab bar gains a Home tab (Home / Stacks / Fleet / Sched / Settings); the global top bar is dropped on this screen, with notifications and a "more" menu rehomed into the masthead. The health-verdict logic is extracted into a shared helper so the phone masthead and the desktop health bar read from one source, with unit tests. All changes are scoped below the md breakpoint or rendered only on the mobile shell; desktop layout is unchanged (verified against the desktop snapshot gate). * feat(mobile): bespoke fleet glance and node detail on phones On phones (below the md breakpoint) the Fleet view now renders a bespoke, masthead-led layout instead of the reflowed desktop workspace: - A fleet masthead leads with the overall fleet-health verdict and a running / cpu / mem summary band, then a list of node cards. The local node is marked with a cyan rail and a "you are here" tag; offline nodes are dimmed. - Tapping a node opens a full-screen node detail: state pill, resource bars (cpu / mem / disk), the stacks running on that node, and an Inspect action that switches to the node. Operators with the right permissions also get a Drain (cordon) action. - The screen polls the fleet overview every 30 seconds; the global top bar is dropped here, with notifications and a "more" menu in the masthead. All changes are scoped below the md breakpoint or rendered only on the mobile shell; desktop layout is unchanged. * feat(mobile): bespoke schedules and settings screens on phones On phones (below the md breakpoint) Schedules and Settings now render bespoke, masthead-led layouts instead of the reflowed desktop workspace: - Schedules: a "next up" glance leading with the next run time and countdown, then upcoming runs grouped by day with a per-action status dot and target. It is read-only on mobile; creating and editing schedules stays on desktop. - Settings: a grouped-card list of every reachable section; tapping one opens it full-screen with a back affordance and a section masthead. The section content itself is the same as on desktop. The settings section switch, lazy-loaded section chunks, and tier gating are moved into a shared component so the desktop and mobile screens render the same section content from one place. The global top bar is dropped on both screens, with notifications and a "more" menu in the masthead. All changes are scoped below the md breakpoint or rendered only on the mobile shell; desktop layout is unchanged. * fix(mobile): show notifications and more-menu on the stack detail header The full-screen stack detail on phones drops the global top bar, but its header was missing the notifications bell and the "more" navigation menu that the other mobile screens carry in their masthead, leaving no way to reach notifications or other destinations while viewing a stack. Render the same header-actions cluster in the detail header (and the loading placeholder), next to the back affordance. Desktop is unaffected. |
||
|
|
2072378396 |
feat(ui): make Fleet, Settings, and the dashboard table usable on mobile (#1331)
* feat(ui): make Fleet, Settings, and the dashboard table usable on mobile Tier 2 of the mobile pass, all gated below the md breakpoint so desktop renders identically: - Fleet: the tab strip scrolls horizontally and the action row (Check Updates / Refresh / Add Node) wraps instead of clipping. - Settings: below md the nav rail is a full-screen list; choosing a section pushes it full-screen with a back affordance (master/detail), matching the stack flow. Desktop keeps the two-pane layout. - Dashboard: the fixed stack-health table scrolls horizontally on a phone. Adds a Playwright desktop visual-regression spec (1280/1440/1920) as a zero-desktop-change gate; its environment-specific snapshots are gitignored. * test(ui): harden the desktop visual-regression gate Mask only the live sidebar ticker and notification count instead of the whole sidebar / top-bar shell, so the gate now proves that shell unchanged too. Lower the pixel budget to 1200 (the only residual churn is in-content live stats); run against seeded / frozen data in CI for a zero-tolerance gate. Adds a data-testid to the activity ticker so it can be masked precisely. |
||
|
|
e8f271f5f6 |
feat(ui): make the core stack flow usable on mobile (#1327)
* feat(ui): make the core stack flow usable on mobile Below the md breakpoint the app collapses to a single full-width column: the stack list is full-screen, tapping a stack opens a full-screen detail with a Health / Logs / Compose segmented control (Logs first) and a back button, and a bottom tab bar switches Stacks, Fleet, Schedules, and Settings. Compose is read-only on a phone with a prompt to edit on desktop. Desktop (md and up) is unchanged: the mobile shell is gated behind a useIsMobile hook plus max-md/md variants, and the stack-detail blocks are shared with the desktop two-pane view so it renders identically. Also generalizes the unsaved-changes guard so leaving a dirty editor (back, tab bar, hamburger) prompts before discarding; adds 44px touch targets on list rows, filter chips, and actions; makes log and shell modals full-screen on mobile; and offsets toasts and the deploy pill above the bottom tab bar. * fix(ui): keep mobile nav in sync when opening views from outside the bottom bar On a phone the sidebar activity actions, the node switcher's Manage Nodes, the profile Settings entry, and the dashboard configuration links set the active view without flipping the mobile surface to content, so the user stayed on the stack list and never saw the destination. Route these through the mobile-aware navigation and settings helpers (a no-op on desktop). |
||
|
|
57fe430db8 |
feat(stacks): add Stack Dossier tab with operator notes and Markdown export (#1326)
* feat(stacks): add Stack Dossier tab with operator notes and Markdown export Add a Dossier tab beside Anatomy and Activity on the stack detail panel. It shows a read-only summary auto-derived from the stack's Compose anatomy (services, ports, volumes, network, restart policy, env file, source) plus an editable form for the context Sencho cannot infer: purpose, owner, access URLs, static IP, VLAN, and firewall, reverse-proxy, backup, upgrade, recovery, and custom notes. Notes persist per stack and per node in a new stack_dossiers table, reached transparently through the remote-node proxy so a remote stack's dossier round-trips to the node that owns it. Reading a dossier needs stack read permission; saving needs stack edit. The tab exports a single Markdown document combining the generated facts and the operator notes, with copy-to-clipboard and download actions; env values are never exported, only variable names and counts. Available on all tiers. The standalone anatomy copy-as-Markdown shortcut is removed since the Dossier export supersedes it. * fix(stacks): gate dossier reads/writes on stack existence; clear dossier on node delete A dossier endpoint validated the stack name but not that the stack exists, so an editor could PUT a dossier for a name with no stack, leaving an orphan row that a later stack of the same name would inherit. Require the stack to exist (existing requireStackExists guard) on the dossier GET and PUT, returning 404 otherwise. Also clear a node's stack_dossiers rows when the node is deleted, alongside the other node-scoped cleanup, so removing a node leaves no orphan dossiers. Docs: scope the "no secret exported" statement to the generated facts (which only ever carry variable names and counts) and clarify that operator notes are exported exactly as written. |
||
|
|
af4083175c |
feat(fleet): add read-only dependency map tab (#1324)
* feat(fleet): add read-only dependency map tab Add a fleet-wide Dependencies tab to Fleet view that maps how stacks, services, networks, volumes, and ports relate, with flags for missing dependencies, port conflicts, orphaned resources, and cross-stack shared resources. Read-only; filterable by stack, node, and flag; collapsed by default with a list-view fallback at scale. The graph is derived at request time from Docker and compose metadata, so no new table or persisted state is introduced. A per-node graph endpoint feeds a hub aggregation endpoint that fans out across the fleet and degrades gracefully, surfacing unreachable or unparseable nodes inline while the rest of the map still renders. * fix(fleet): harden dependency map flag detection and remote merge Address review findings on the dependency map: - Port-conflict detection now does pairwise host-scope overlap, so an unrelated bind on the same port and protocol but a different specific host IP is no longer flagged, and the flag lands on the exact scoped port node. - A running service's depends_on target is only considered satisfied when it is actually running, so a crashed (exited) dependency is surfaced while a deliberately stopped stack stays quiet. - Declared external networks and volumes are reported missing when they do not exist on the host instead of being assumed present. - The hub deep-validates each remote node-graph payload before merging, so a reachable-but-malformed remote degrades to a single node error rather than failing the whole fleet map, and the validation failure is logged. - Searching or filtering on a network, volume, or port now also reveals the services that claim it and their stacks. |
||
|
|
13b6acab16 |
feat(stacks): copy stack anatomy as Markdown (#1323)
* feat(stacks): copy stack anatomy as Markdown Add a "copy md" action to the Stack Anatomy panel that exports the parsed compose summary as a deterministic Markdown document: services, ports and volumes tables, restart policy, env file with its variable count, missing variables, network, and source. Useful for pasting a stack's wiring into a README, a Git repository, Obsidian, BookStack, or a support thread. The export carries only env variable names and counts, never the values stored in .env files. * fix(stacks): escape backslashes in anatomy Markdown table cells escapeCell escaped pipes but not the backslash itself, so a backslash in a cell value (for example a Windows path) could defeat the pipe escaping and break the rendered table row. Escape the backslash first, then pipes, then collapse newlines. Adds a regression test. * fix(stacks): correct git source attribution and CR handling in anatomy export Gate the Anatomy panel's git source on its owning stack so a slow git-source response for a previously selected stack can no longer render or be exported under a different stack. Also collapse lone carriage returns (not just CRLF and LF) in Markdown table cells so a stray CR cannot break a table row. |
||
|
|
7df5ffde54 |
chore(main): release 0.90.0 (#1317)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com>v0.90.0 |
||
|
|
86bfc108ae |
fix(security): resolve open CodeQL path-injection and temp-file alerts (#1322)
Re-establish the path-containment barrier inline at the backup readdir sink in restoreStackFiles. The sink previously built backupDir through the getBackupDir helper, whose stack-name validation the static analyzer does not trace, leaving a flagged path-injection sink. The barrier now resolves backupDir against its root and asserts containment inline, the same pattern already used in backupStackFiles. Behavior is unchanged for valid stack names (already validated one line above by resolveStackDir). Scope the js/insecure-temporary-file rule out of e2e/** in the CodeQL config. End-to-end fixtures must seed files into the backend's COMPOSE_DIR so the API under test can read them back; that path is a fixed location under /tmp in both CI and local dev, which the rule flags. A randomized temp directory does not apply because the backend resolves against its own COMPOSE_DIR. Production code is still analyzed. |
||
|
|
ce08a593d7 |
feat(settings): reorganize the settings hub into domain groups (#1321)
* refactor(settings): split System Limits and regroup the hub System Limits had grown into a grab-bag of host alert thresholds, Docker cleanup, and mesh data-plane controls under one mislabeled section. Split it into Host Alerts, Docker & Storage, and Fleet Mesh, and split Developer into Developer Diagnostics and Data Retention. Reorganize the sidebar into ten domain groups: Personal, Access, Infrastructure, Monitoring, Notifications, Automation, Organization, Security, Operations, Help. Each section now saves only its own keys, so a concurrent edit in one section no longer clobbers another. Data Retention sends the audit-log window only on a paid plan, matching the field's existing visibility, so a Community save no longer fails on a key the operator cannot set. NumberChip moves to a shared module and the toggle reuses the existing shared component. The /settings API is unchanged. * test(settings): cover registry structure and per-section save payloads Add structural invariants for the ten-group registry (every item maps to a real group, ids are unique, the System Limits and Developer splits land in the right groups with the right gates, renamed labels and the Registries paid gate hold) and per-section payload tests asserting each split section patches only its own keys, including the Community path where Data Retention omits the paid audit-log key. * docs(settings): document the regrouped settings hub Rewrite the settings reference for the ten-group layout, replace the System Limits page with Host Alerts, Docker & Storage, and Fleet Mesh, and document the prune-on-update, reclaimable-space banner, and mesh auto-recreate settings that were previously undocumented. Update the Settings navigation breadcrumbs across the feature docs and refresh the affected screenshots. * fix(settings): show Access sections as instance-global, not operator-scoped License, Users, SSO, and API Tokens are instance-global settings but the masthead scope label rendered them as operator-scoped because it keyed off the old Identity group. Only Personal sections (account, appearance) are operator/browser-scoped now; everything else reads as global. Also add a compile-time exhaustiveness guard to the section switch so a future SectionId added without a matching case fails the build instead of silently rendering a blank panel. * docs(settings): remap remaining settings breadcrumbs to the new groups Update the navigation breadcrumbs that still pointed at the removed Identity, Alerts, and Advanced groups: API Tokens and Users now sit under Access, Webhooks under Automation, Labels under Organization, App Store under Infrastructure, Appearance under Personal, and scan policies under Security > Vulnerability Scanning. Correct the settings reference scope note so Access reads as global. * docs(settings): remap renamed-section breadcrumbs across feature docs Sweep every feature, operations, getting-started, and reference page for navigation paths that still named the renamed settings sections, and point them at the current ones: Security becomes Security > Vulnerability Scanning, Notifications becomes Notifications > Channels, Routing becomes Notifications > Notification Routing, and Developer becomes Operations > Developer Diagnostics (with its retention windows under Operations > Data Retention). App Store moves under Infrastructure and the four-group overview in the getting-started intro is rewritten to the ten groups. Separators each page already used are preserved. |
||
|
|
f7f3afe05a |
feat(stacks): one-click import for stray compose files (#1320)
* feat(stacks): move discovered import candidates into place The guided import flow previewed loose and nested compose files but could not act on them, so it only told the user where to move files by hand. Add an opt-in "Move into place" action: relocate a loose-root file into its own <name>/ subfolder, or promote a nested stack directory one level up, so Sencho's filesystem discovery lists it as a stack. The file stays a plain compose file on disk; nothing is captured into a store. The move re-derives the candidate from a fresh scan and matches by location, validates the destination name and containment, resolves symlinks before the rename, and never overwrites an existing stack. Backend and frontend both gate the action on stack:create. Also fix the rescan flicker: scan results now stay on screen while a rescan runs (only the Rescan button shows progress) instead of the whole panel collapsing to a spinner, and an empty rescan surfaces a toast. * fix(stacks): make import-move destination creation atomic The loose-root branch created the destination directory with mkdir recursive after an access() existence precheck. If the destination appeared between the check and the create, recursive accepted the existing directory and the following rename could overwrite a same-named compose file inside it, so the intended conflict response never fired. Use a non-recursive mkdir so a destination that already exists raises a conflict instead of being merged into. Add a regression test that forces the precheck to miss and asserts the existing file is left intact. * fix(stacks): only offer not-yet-imported compose files in the import tab The import tab listed every compose file in the compose directory, including ones that are already stacks (a top-level subfolder with a compose file), which just duplicated the sidebar. The scan now skips those and surfaces only files that still need importing: a compose file loose at the compose-dir root, or one nested a folder too deep. Also harden the move-into-place write path that turns a stray file into a stack: a failed rename after the destination folder is created now rolls back the empty folder, so a retry is not blocked by a false "already exists" conflict, and the move switches on an exhaustive set of placements so a new one cannot silently take the wrong branch. The sidebar refreshes after a move so the imported stack appears right away, and the docs describe import as relocating a file, not capturing running containers. * fix(stacks): reject a nested import whose compose file escapes the base The move-into-place path for a nested compose file validated only the parent directory's real path, not the compose file itself. A directory that is real and inside the compose base but holds a compose file symlinked outside the base would survive the directory move and become a stack whose compose file still points outside the base, which the editor read path would then follow. The move now resolves the compose file too and refuses it unless it stays inside the resolved source directory, matching the loose-root check and the scan's preview reader. * fix(stacks): satisfy CodeQL path and log analysis in import-move The import-move write path built its destination directory from the user-provided stack name through resolveStackDir, whose containment barrier is wrapped in a helper that static analysis does not credit, so every filesystem sink on the destination was flagged as path injection. Re-establish the resolve-against-the-safe-base plus startsWith barrier inline at the sinks, matching the read and backup paths in the same file, and route the relocated file path through the same check. The name is already restricted to an alphanumeric, hyphen, and underscore allowlist, so the containment can never actually fail; this only makes the existing safety visible to the analyzer. Also log the move route's error as a sanitized message rather than the raw error object, so a name embedded in an error message cannot forge log lines. |
||
|
|
28ea610e81 |
refactor(masthead): remove stat-tile hover tooltips (#1319)
Drop the cursor-following hover tooltips from the masthead stat tiles: the RUNNING tile on the Home dashboard (the managed / external / exited breakdown) and the CONTAINERS tile on the Fleet view (the running / total split). Both revert to plain stat tiles, matching the CPU and MEM tiles beside them. The dashboard and fleet docs are updated to match and the orphaned screenshot is removed. |
||
|
|
308949282c |
feat(resources): reclaim banner controls and accurate reclaim math (#1318)
* feat(resources): reclaim banner controls and accurate reclaim math Make the Resources Hub reclaim banner match what it advertises and give operators control over when it appears. - "Review & prune" now reclaims every category the banner lists (unused images, stopped containers, and dangling volumes) instead of images only, so the banner clears in one action. Pruning runs volumes first, while stopped containers still reference their named volumes, so a stopped stack's data is never cascaded into deletion. - Add a "Show reclaimable-space banner" toggle under Settings, System, Docker hygiene (on by default, per node) and a dismiss control on the banner that snoozes it until the reclaimable total grows again. - Fix the reclaimable-space math: count only containers a prune can actually remove (created, exited, dead) and size them by their writable layer, so a small, un-prunable remainder no longer keeps the banner up. * fix(resources): show the reclaim banner when the settings fetch fails A failed or empty /settings load left the banner's enabled flag at the previously active node's value, so switching from a node with the banner turned off to a node whose /settings errored kept the new node's banner hidden. Set the flag unconditionally after the staleness guard so a failed fetch falls back to the default-on state for the current node. |
||
|
|
716daf77d0 |
feat(updates): auto-prune dangling images after updates (#1316)
* feat(updates): auto-prune dangling images after updates Each update pulls a fresh image and recreates containers, leaving the replaced image behind as a dangling layer that previously had to be pruned by hand. A new "Prune dangling images after updates" toggle under Settings > System > Docker hygiene reclaims these automatically. The setting is on by default and opt-out. When enabled, a successful stack update (manual or scheduled) and a Sencho self-update each remove the dangling image layers they orphaned. Only untagged layers are touched; tagged images, volumes, and data are never removed. The toggle requires an admin account and is per node: each instance honors its own value, so a remote node self-update applies that node's own preference. A prune failure never affects the update result: on the stack path it is caught and logged after the update has already succeeded, and on the self-update path the helper-shell prune runs only after a clean recreate and cannot change the exit code or the recorded update error. * security(self-update): shell-quote label-derived values in helper command Address review feedback on the prune-on-update change: - The self-update helper command interpolated the compose service name and config-file paths (both read from Docker Compose labels) straight into a shell string. Shell-quote them via shQuote so a label carrying shell metacharacters stays inert data and cannot break the exit-code capture, error-file write, or prune guard. - Correct the settings copy and docs: the prune is a standard dangling-image prune, so it reclaims every untagged layer on the node, not only the one the current update orphaned. Tagged images, volumes, and data remain untouched. - Add tests: shell-metacharacter neutralization and prune-output suppression in the self-update command, and an atomic-update case asserting a prune failure does not trigger a rollback. * fix(updates): omit the reclaim figure when the daemon reports zero bytes End-to-end testing on a Docker daemon backed by the containerd image store showed the post-update prune removing a dangling image while the prune API returned SpaceReclaimed=0, so the stream printed "reclaimed 0.0 MB" even though an image was removed. Show the reclaimed figure only when the daemon reports a non-zero value; otherwise the line reads "=== Pruned dangling images ===". The overlay2 store still reports real figures and shows them. Add a test covering both branches. |
||
|
|
622af7e0b3 |
fix(theme): make sheets and dialogs track the active theme (#1315)
* fix(theme): make sheets and dialogs track the active theme Portaled panels (scan-history and other sheets, dialogs, confirm modals) only partially followed the theme: their popover surface was a flat per-theme literal, so OLED mode never reached true black inside them and the contrast control did not move their fill. The accent stopped at the thin rail because the ambient glow was painted only on the main app shell, never inside a portaled panel, and the overlay scrim was a hardcoded black. - Derive the popover surface from per-theme lightness and alpha plus the contrast spread, the same way the page background is derived, so every floating surface tones with the active theme (including OLED true black) and deepens with contrast. Dim and Light are unchanged at the default contrast. - Add an accent wash to sheets and large dialogs so the chosen accent reads inside them, matching the page. Small dropdowns and tooltips keep the themed tone without the wash. - Replace the hardcoded modal and sheet scrim with a theme-aware token that softens to a light dim in the Light theme. Tested across Dim, OLED, and Light with multiple accents and contrast levels. * fix(theme): keep the command palette token-only (no accent wash) The command palette renders through DialogContent, so it picked up the accent wash meant for content sheets and dialogs. It is a command menu, so it should stay token-only like dropdowns and tooltips. Add an optional panelGlow flag to DialogContent (on by default, so every other dialog is unchanged) and turn it off for the palette. |
||
|
|
5d508b9416 |
fix(blueprints): keep the edit sheet body within the sheet width (#1314)
The blueprint detail sheet embeds a code editor in its Edit form. The sheet body scrolls inside a Radix ScrollArea whose default wrapper sizes to its content, so the editor's intrinsic width pushed the body past the sheet edge and the right side (the editor and the Save/Cancel buttons) was clipped. Add an opt-in constrainBodyWidth prop to SystemSheet that bounds the scrolling body to the sheet width and lets a wider child scroll horizontally, and enable it on the blueprint sheet. The prop defaults off, so every other sheet is unchanged. |
||
|
|
b4cca9b995 |
fix(blueprints): allow deleting blueprints stuck on awaiting-confirmation deployments (#1313)
* fix(blueprints): allow deleting blueprints stuck on awaiting-confirmation deployments Withdrawing a stateful deployment removed its row, but the reconciler re-created it as "Awaiting confirmation" for any node still matching the selector. The delete guard counted that recreated row as blocking, so a stateful blueprint could never be deleted: every withdraw came back as a pending review and delete kept refusing. The delete guard now blocks only on deployments that have a live stack on a node (active, drifted, correcting, evict_blocked, or a pending review that was previously deployed). A never-deployed "Awaiting confirmation" deployment no longer blocks; the delete path's best-effort withdraw-all loop and the foreign-key cascade clear those rows. Live stateful deployments still require explicit withdrawal first, so the snapshot-vs-destroy choice is always made by the operator. Updates the delete dialog copy to match and adds delete-guard coverage. * fix(blueprints): harden delete against destroying unmanaged or live stacks Address two issues found while auditing the delete guard: - A never-deployed deployment (for example a reconciler-created pending review) no longer blocked delete, but the route's withdraw-all loop still ran the withdraw primitive for it. withdrawFromNode proceeds on a missing marker, so deleting the blueprint could down and delete an unmanaged same-name stack on the node. The loop now withdraws only the stacks Sencho deployed and still owns, and skips never-deployed, name_conflict, and withdrawn rows. - A stack that was deployed then failed keeps its last_deployed_at, but the guard's status list did not include failed, so it could be deleted without the snapshot-vs-destroy choice. The guard now keys on last_deployed_at (a stack we deployed and own) rather than a status list, so any owned live stack blocks regardless of its current status, while never-deployed, name_conflict, and withdrawn rows do not. Adds delete-guard coverage for these paths, including that withdraw is never invoked for an unmanaged row and is invoked for an owned one. |
||
|
|
1f859d26d2 |
chore(main): release 0.89.0 (#1308)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com> Co-authored-by: Anso <dev@saelix.com>v0.89.0 |
||
|
|
37af4a2087 |
refactor(fleet): standardize empty-state and header layout across Fleet tabs (#1312)
Extract the Secrets tab's header and empty-card markup into shared primitives (FleetTabHeading, FleetEmptyState, FleetEmptyCard) and adopt them across Snapshots, Deployments, Routing, Federation, and Secrets so the Fleet area presents one consistent layout. The empty card sits vertically centered below a title/subtitle header. Deployments keeps its onboarding steps inside the shared shell; Federation's sections are unchanged. |
||
|
|
81858a0bb0 |
feat(mesh): refine the Routing tab and add per-route removal (#1311)
* feat(mesh): refine the Routing tab and add per-route removal
Tighten the Fleet > Routing tab so its node cards and diagnostics read
honestly for proxy-connected fleets, and remove a couple of dead ends.
- Node cards drop the "pilot connected/offline" line, which was meaningless
for nodes that connect over the HTTP API proxy. The compact card drops the
matching agent cell and now reads stacks / aliases / bridge.
- Enabling mesh on a proxy node shows a transient "Connecting" state and
settles to meshed on its own, instead of flashing "Degraded" with a manual
refresh while the bridge finishes dialing.
- The alias detail sheet gains a "Remove from mesh" action that opts the
alias's owning stack out (the confirmation says how many aliases that
drops), and a "Topology" tab. The standalone topology sheet and its
opt-in-sheet shortcut are removed in favor of the tab.
- The "Add stack" affordance stays reachable on a meshed node, so a node
that already has aliases is no longer a dead end.
- Diagnostics and the alias detail sheet show a transport-aware line
("local", "API proxy bridge", or "Pilot tunnel" with its state) instead of
a fixed "Pilot tunnel" label.
Adds unit coverage for the new state derivation, transport descriptor, the
enable auto-converge, and the admin-gated removal, and updates the mesh docs.
* fix(mesh): harden Routing tab toggle and alias sheet against async races
Independent review surfaced two narrow async races in the new code.
- RoutingNodeCard: cancel the converge re-poll batch at the start of any
toggle (so a slow disable can't let a prior enable's re-polls fire), and
guard the toast, refresh, timer scheduling, and setToggling behind a mounted
ref so nothing runs after the card unmounts while the enable POST is still
in flight.
- MeshRouteDetailSheet: re-check the cancelled flag after reading the
diagnostic body, not just before it. Reading the body is itself async, so a
superseded alias's diagnostic could otherwise call setDiag and expose Remove
for the wrong stack.
Adds tests for unmount-before-enable-resolves and the deferred-diagnostic
alias switch.
|
||
|
|
9d3055049f |
fix(ui): center create-stack label and drop duplicate plus on mesh CTA (#1310)
The Create Stack button left a redundant `mr-2` on its Plus icon. The Button base already applies `gap-2`, so the margin doubled the spacing and pushed the centered icon-plus-label block right of center. Removing `mr-2` restores the single correct gap. The meshed-state routing CTA rendered both a Plus icon (via ctaIconFor) and a literal "+ " prefix in the text, showing two plus glyphs. Dropped the literal prefix so only the icon renders. |
||
|
|
865d792874 |
feat(pricing): collapse to two tiers (#1309)
* feat(pricing): collapse to two tiers (Community + Admiral) Collapse Sencho's pricing from three tiers (Community / Skipper / Admiral) to two: a generous free Community tier and a single paid Admiral tier. The Skipper tier is removed. Now free in Community: auto-heal, auto-update, scheduled operations, webhooks, notification routing, Fleet Actions and bulk operations, SSO preset providers (Google / GitHub / Okta), unlimited users with admin and viewer roles, and deploy safety (atomic deploys, auto-rollback, and one-click rollback). Admiral (paid) is focused on running and governing a fleet: blueprints, Fleet Secrets, deploy enforcement, vulnerability report export, audit log, host console, private registries, mesh networking, node cordon, managed cloud backup, LDAP / Active Directory SSO, and the advanced RBAC roles (deployer, node-admin, auditor) with per-resource scoped assignments. Internally the license variant distinction is removed so tier is binary (community / paid). License validation still verifies the Lemon Squeezy store and product before granting paid status. Docs and the contributor guide are updated to the two-tier model. * docs(pricing): correct licensing page to two-tier pricing and tidy stale tier wording The licensing docs page kept the old Admiral pricing plus a Founder Lifetime column and an Enterprise paragraph after the two-tier collapse. Update it to $12/month or $99/year, drop the lifetime and Enterprise content, and link to the pricing page for current pricing. Also fix stale "Skipper" wording in CLA.md, SUPPORT.md, one test title, and three test comments. Historical CHANGELOG entries and the retired-Skipper license-guard test are intentionally left as-is. * docs: align licensing and SSO pages with the two-tier model Correct the SSO overview so the Google, GitHub, and Okta presets read as available on every tier, matching the provider table; only LDAP and Active Directory require Sencho Admiral. Remove the lifetime-plan references from the licensing, settings, and troubleshooting pages so they reflect subscription-only Admiral pricing. * fix(rbac): omit scoped permissions from /me on the Community tier Scoped role assignments only take effect on the paid tier, but GET /api/permissions/me returned them unconditionally, so a downgraded instance with leftover assignments rendered per-resource affordances the API then rejected with 403. The endpoint now mirrors the permission middleware and includes scoped permissions only on the paid tier. Adds a regression test covering the downgrade case. * docs: use custom-pricing wording on the contact page The two-tier model has no Enterprise tier; reword the contact page's enterprise pricing/deals to custom pricing/deals so it does not imply a tier that no longer exists. |
||
|
|
7b78cb9cc9 |
chore(deps): bump the all-npm-backend group in /backend with 9 updates (#1306)
Bumps the all-npm-backend group in /backend with 9 updates: | Package | From | To | | --- | --- | --- | | [axios](https://github.com/axios/axios) | `1.16.1` | `1.17.0` | | [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) | `1.38.3` | `1.38.4` | | [otplib](https://github.com/yeojz/otplib/tree/HEAD/packages/otplib) | `13.4.0` | `13.4.1` | | [systeminformation](https://github.com/sebhildebrandt/systeminformation) | `5.31.6` | `5.31.7` | | [eslint](https://github.com/eslint/eslint) | `10.4.0` | `10.4.1` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.0` | `8.60.1` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.7` | `4.1.8` | | [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1054.0` | `3.1061.0` | | [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1054.0` | `3.1061.0` | Updates `axios` from 1.16.1 to 1.17.0 - [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.16.1...v1.17.0) Updates `isomorphic-git` from 1.38.3 to 1.38.4 - [Release notes](https://github.com/isomorphic-git/isomorphic-git/releases) - [Commits](https://github.com/isomorphic-git/isomorphic-git/compare/v1.38.3...v1.38.4) Updates `otplib` from 13.4.0 to 13.4.1 - [Release notes](https://github.com/yeojz/otplib/releases) - [Commits](https://github.com/yeojz/otplib/commits/v13.4.1/packages/otplib) Updates `systeminformation` from 5.31.6 to 5.31.7 - [Release notes](https://github.com/sebhildebrandt/systeminformation/releases) - [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md) - [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.31.6...v5.31.7) Updates `eslint` from 10.4.0 to 10.4.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.4.0...v10.4.1) Updates `typescript-eslint` from 8.60.0 to 8.60.1 - [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.60.1/packages/typescript-eslint) Updates `vitest` from 4.1.7 to 4.1.8 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest) Updates `@aws-sdk/client-ecr` from 3.1054.0 to 3.1061.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.1061.0/clients/client-ecr) Updates `@aws-sdk/client-s3` from 3.1054.0 to 3.1061.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-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1061.0/clients/client-s3) --- updated-dependencies: - dependency-name: axios dependency-version: 1.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: isomorphic-git dependency-version: 1.38.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: otplib dependency-version: 13.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: systeminformation dependency-version: 5.31.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: eslint dependency-version: 10.4.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: typescript-eslint dependency-version: 8.60.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: vitest dependency-version: 4.1.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: "@aws-sdk/client-ecr" dependency-version: 3.1061.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: "@aws-sdk/client-s3" dependency-version: 3.1061.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anso <dev@saelix.com> |
||
|
|
e3a9cf922f |
chore(deps-dev): bump the all-npm-root group with 3 updates (#1305)
Bumps the all-npm-root group with 3 updates: [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli), [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) and [otplib](https://github.com/yeojz/otplib/tree/HEAD/packages/otplib). Updates `@commitlint/cli` from 21.0.1 to 21.0.2 - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.0.2/@commitlint/cli) Updates `@commitlint/config-conventional` from 21.0.1 to 21.0.2 - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.0.2/@commitlint/config-conventional) Updates `otplib` from 13.4.0 to 13.4.1 - [Release notes](https://github.com/yeojz/otplib/releases) - [Commits](https://github.com/yeojz/otplib/commits/v13.4.1/packages/otplib) --- updated-dependencies: - dependency-name: "@commitlint/cli" dependency-version: 21.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-root - dependency-name: "@commitlint/config-conventional" dependency-version: 21.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-root - dependency-name: otplib dependency-version: 13.4.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-root ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anso <dev@saelix.com> |
||
|
|
a521db2ba8 |
chore(deps): bump golang from 1.26.3-alpine to 1.26.4-alpine (#1304)
Bumps golang from 1.26.3-alpine to 1.26.4-alpine. --- updated-dependencies: - dependency-name: golang dependency-version: 1.26.4-alpine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anso <dev@saelix.com> |