mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
3b027957c4369a4d8c0ea5513b9e631b1bf2508b
1752 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
69ba0e6d21 |
fix(stacks): harden stack file path containment against symlink escapes (#1415)
* fix(stacks): harden stack file path containment against symlink escapes The legacy managed-stack methods enforced path containment lexically (path.resolve + startsWith), which does not follow symlinks. A stack directory under the compose root that is itself a symlink or junction could let a managed-file operation (write compose.yaml/.env, delete a stack, backup, restore, snapshot) follow the link and read, write, or delete a file of the same name outside the compose root. Add a realpath-based containment guard that walks up to the deepest existing path component, confirms its canonical location is inside the canonical compose root, and rejects both an out-of-tree resolution and a dangling symlink (which a write or mkdir would still follow). The guard runs at every legacy managed-stack sink, alongside the existing lexical barriers. A legitimately symlinked compose root is not a false positive because both sides are canonicalized through the same link, and the guard is a no-op for not-yet-created targets so stack creation and the flat-to-directory migration are unaffected. * fix(stacks): satisfy the path-injection sanitizer in the symlink containment guard assertRealWithinBase resolves a user-derived path and probes it with realpath/lstat to run the containment check, which static analysis flags as path injection because the probes lacked the inline barrier its sanitizer recognizes. Add the canonical path.resolve + startsWith barrier at the top of the helper, the same form every other sink in this file uses, and seed the realpath walk from the sanitized value. Behavior is unchanged: callers always pass an absolute, already-contained path, so the barrier is a no-op pre-check for them, and the realpath walk still catches the symlink and dangling-link escapes. |
||
|
|
f91227dada |
fix(search): don't open the command palette via Cmd/Ctrl+K while typing (#1414)
The global command palette's Cmd/Ctrl+K shortcut fired even when a text field or the code editor held focus, stealing focus mid-edit and, on macOS, clobbering the native Ctrl+K delete-to-end-of-line. Guard the handler with isInputFocused() so the shortcut opens search only when the user is not typing in a field; the toolbar search icon still opens it from anywhere. Add unit tests for the keyboard path. |
||
|
|
1c5b2715b8 |
fix(editor): suppress global hotkeys while the code editor is focused (#1413)
Typing a single-key shortcut (a/h/u/p/b) in the compose editor opened that shortcut's action instead of inserting the character, but only in Chrome. The shared isInputFocused() guard recognized inputs, textareas, and contentEditable elements; in Chromium the Monaco editor uses the EditContext API whose focused surface is a plain focusable div, so the guard missed it and the hotkey handlers fired. Safari falls back to a hidden textarea, which the guard already caught. Match any focus inside the .monaco-editor container so the guard covers the EditContext surface, bringing Chrome to parity with Safari for both global hotkey systems. Add a unit test for the guard. Closes #1410 |
||
|
|
37e6e48b40 |
feat(files): copy & duplicate, bulk actions, disk-backed uploads, and an accessible file tree (#1409)
* perf(files): spool uploads to disk instead of buffering in memory
Switch the stack file-explorer upload from multer memoryStorage to
diskStorage and stream the spooled temp file through the file-root
gateway, so an upload is never held fully in RAM. Authorization and
root resolution now run before multer spools, so an unauthorized or
read-only-root request is rejected without writing a temp file, and the
spool is removed on every exit path. The named-volume helper write
verifies the written byte count, since cat cannot report a short write.
* feat(files): copy and duplicate files in the explorer
Add a copy capability to the stack file explorer: a same-folder
Duplicate (auto-suffixed name) and a "Copy to..." destination picker,
on both filesystem and named-volume roots. Copying is within-root,
symlink-leaf-safe, blocks a directory copy into its own subtree, and
refuses to create a protected name (compose/.env) at the stack root
while still allowing a protected file to be duplicated under a new name.
* feat(files): make the file tree keyboard accessible
Bring the stack file explorer tree to the WCAG tree pattern: rows are
treeitems carrying aria-level, aria-selected, and aria-expanded, with a
single roving tabindex and full keyboard navigation (arrow keys,
Home/End, Enter/Space) over a flattened visible-node list that stays in
lockstep with the rendered rows. A polite live region announces the
selected file. No visual change to the tree.
* feat(files): bulk select, delete, move, and download files
Add multi-select to the stack file explorer (checkboxes plus Shift and
Ctrl/Cmd click over the visible order) driving three bulk actions:
delete, move, and download as a streamed .tar.gz. All run within the
active root on both filesystem and named-volume backends, report
per-item results so partial failures surface (with the failed items
kept selected for retry), normalize ancestor/descendant selections
server-side, and cap the archive entry and byte counts before any
bytes are streamed. Protected compose/.env files are excluded from
bulk delete and move but may still be downloaded.
* docs(files): document copy, bulk actions, and keyboard navigation
Add the copy/duplicate and multi-select bulk delete/move/download
sections to the Files & Volumes page, a keyboard-navigation note for the
tree, an updated context-menu reference, and bulk troubleshooting entries.
* fix(files): inline path-injection barriers at the new file-op sinks
CodeQL js/path-injection does not credit the wrapped isPathWithinBase
containment check, so the new copy/bulk/disk-upload flows tripped the
gate. Inline the canonical path.resolve + startsWith barrier at the
realpath sink in resolveSafePathWithin (covers every user-relPath flow)
and confirm the multer spool path resolves within UPLOAD_TMP_DIR before
unlinking it or streaming it onward. Behavior is unchanged; the paths
were already validated.
* fix(files): guard the ancestor-walk realpath sink too
The first barrier covered realpath(target), but the ENOENT ancestor
walk re-derives the path via path.dirname, which static analysis treats
as a fresh tainted value. Add the same inline containment barrier before
that realpath and resolve the root case via the untainted base, so the
only tainted realpath input is one the startsWith check has cleared.
Behavior is unchanged.
* fix(files): resolve the root case off the taint path in the ancestor walk
The compound guard on existing (the same variable as the startsWith
subject) was not credited as a sanitizer. Handle the root case before
the barrier by resolving the untainted base directly, leaving a plain
canonical startsWith guard on the strictly-within ancestor. Behavior is
unchanged.
* fix(files): harden helper-backend bulk download and uploads
Address three issues found in the named-volume (helper) backend:
- Bulk download could send 200 headers before discovering a file the
helper download path refuses, tearing the archive mid-stream. The
prewalk now rejects symlinks, non-regular ("other") entries, and
files over the per-file download cap before any header (400/413).
FileEntry gains an 'other' type so non-regular entries stay distinct
from regular files as they pass through the gateway.
- The helper directory listing was fully buffered before the archive
entry cap could fire. listDir now accepts a limit; the list script
stops after limit+1 rows and the gateway reports truncation.
- A stdin pipeline error during a helper upload masked the container's
real nonzero exit code (and its 4xx mapping) as a generic 500. The
nonzero exit now wins; the masked stream error is logged.
* feat(files): add a New file toolbar button with server-enforced create-only
The stack file explorer could create a folder from a toolbar button but a
new file only from a folder's right-click menu, so a file could not be
created at the stack root at all. Add a New file toolbar button beside New
folder, targeting the current directory.
Creating a file now routes through a new createEmptyStackFile helper that
posts a zero-byte file through the existing upload endpoint with overwrite
off, so the server's exclusive-create path rejects an existing name instead
of clobbering it. A file collision surfaces inline in the dialog; a folder
collision and other failures surface as a toast.
* fix(files): widen the tree row hit area and add horizontal scroll for long names
Right-clicking a file tree row only opened the Sencho context menu when the
click landed on the filename; the rest of the row fell through to the native
browser menu, and long names were truncated with no way to read them.
Make each row span the full pane width (and grow with its content) so the
whole row is the context-menu trigger, and let the tree scroll horizontally
so a long name is reachable instead of clipped. A new opt-in horizontal prop
on ScrollArea adds the styled horizontal scrollbar without clamping content
width.
* docs(files): document the New file button, full-row right-click, and long-name scrolling
* test(files): cover createEmptyStackFile targeting the stack root
Add an API-layer case for the empty-directory (stack root) create path, the
primary reason the New file toolbar button exists, so a regression in the
root-level URL would be caught at unit speed rather than only in e2e.
|
||
|
|
9480cc98bb |
fix(rate-limit): key authenticated requests by verified JWT, not unverified decode (#1412)
The hybrid rate-limit key generator bucketed authenticated requests by a username read from an unverified jwt.decode of the session cookie or Bearer JWT. One source could fragment the per-source cap by rotating a forged JWT with a varying username, minting a fresh bucket per value. Each forged request is still rejected at auth, so this is DoS amplification, not an auth bypass; the same class as the API-token vector hardened earlier. Verify the JWT signature against the cached signing secret before keying by username; forged, expired, or otherwise invalid credentials fall back to per-IP. Enforce strict bearer-over-cookie precedence so a present-but-invalid Bearer cannot be rescued into a valid cookie's bucket. The verification helper fails closed: any error degrades to per-IP rather than throwing out of the key generator. |
||
|
|
8d9e6574cc |
feat(appearance): add Calm/Signature visual style, readability mode, and chart palette (#1407)
* feat(appearance): add Calm/Signature visual style, readability mode, and chart palette Turn the "too intense / italic headers hurt / the security graph fights my eyes" feedback into a token-driven Visual style with Calm as the new default and Signature one click back to the prior look. - Heading family routes through a `.font-heading` utility driven by `--font-heading`/`--heading-style`: operational headings render upright in the interface face under Calm and italic Instrument Serif under Signature. Base rule sets family + style only, so each call site keeps its own weight/tracking and Signature stays a true no-op; the Calm lift is a `[data-headings="clean"]` descendant rule. Brand lockup, empty-state heroes, and onboarding stay serif. - Severity charts resolve through `--sev-*` tokens with Muted, Heat, and Signature palettes; FindingsByType routes its series through the severity ramp plus a neutral so no brand-cyan sits next to rose. The risk trend flattens its gradient under Muted/Heat/reduced and keeps the gradient under Signature. - Appearance settings gain Visual style cards, a Security visualization palette, a Readability master toggle, a Motion & effects group, and a "Reset to default" button (restores the Calm axes, disabled while readability is on). Contrast moves under Readability and Ambient glow under Motion & effects. A card is selected only while the stored sub-axes match its preset, so a custom combination de-selects both. - The topbar Theme quick-switch swaps the interface/data font pickers for a Visual style switch and a Readability toggle (text size kept); its footer Settings link jumps straight to Appearance. - Readability is a sticky master that forces the calm resolution and a contrast lift at apply time without mutating the stored sub-axes. - New users default to Calm; any pre-existing persisted appearance state keeps the Signature look. The pre-paint script mirrors the store. - SegmentedControl gains a `disabled` prop and a nullable value (no active segment for a custom combination, with a roving-tabindex keyboard anchor). Adds unit/component coverage for the store, migration, chart shape logic, the disabled control, the reset/de-selection, and the quick-switch. * fix(appearance): migrate Blueprint serif headings and surface readability locks - Migrate the two operational Blueprint headings (catalog tile name, drift-policy option title) from font-serif italic to the .font-heading utility; the first pass only covered font-display, so Calm still left these italic. font-serif and font-display both resolve to the same display face, so this is the same fix. - Lock the Visual style cards under Readability (parity with the topbar switch and the on-screen guidance to turn Readability off to choose a style by hand). - Lock the Border brightness slider under Readability and show its forced +0.03 readout, since Readability overrides the stored value; dragging it previously appeared to do nothing. - Correct the Appearance docs sentence for the topbar quick switch (it listed fonts; the quick switch now carries visual style, readability, and text size). |
||
|
|
31aa1d2d37 |
chore(gitignore): drop the obsolete frontend/DESIGN.md ignore entry (#1408)
The design system reference now lives under docs/design/, which .gitignore already covers, so the standalone frontend/DESIGN.md ignore line points at the old location and is no longer needed. |
||
|
|
f9c6c5fd09 |
fix(drift): reconcile the drift ledger on deploy and timestamp its history (#1405)
* fix(drift): reconcile the drift ledger on deploy and timestamp its history
The drift ledger (persisted history + activity timeline) only advanced
when someone clicked re-check on a stack's Drift tab, so the history could
sit indefinitely out of sync with the live status: a stack reading
"drifted" live while its history still said "resolved". Two corrections:
- Deploy and update reconcile the ledger against the just-deployed runtime
(the rollback route re-deploys through deployStack, so it is covered),
resolving what the change fixed and recording what it left.
- Every authoritative reconcile stamps the dossier last-checked time, and
the Drift tab labels its history "checked {time}" so a stale finding
reads as history, not a claim about the live status above it.
Adds the last_drift_check_at column and tests across the ledger reconcile
stamp, reconcileStack, the deploy hook, and the panel.
* fix(drift): stamp last-checked inside the ledger transaction
Move the dossier last-checked stamp into the same transaction as the
finding insert/resolve, so the "checked {time}" the Drift tab shows can
never persist without the ledger update it describes. The stamp still runs
on a no-op authoritative check (a transaction that only stamps), keeping
the history "as of" honest. Adds a test that a failed deploy does not
reconcile the ledger.
|
||
|
|
b9d8e9f490 |
feat(stacks): browse and edit mounted volume files in the explorer (#1403)
* feat(stacks): browse and edit mounted volume files in the explorer Reposition the stack file explorer around runtime configuration access: discover a stack's declared mounts and expose each as a safe, stack-scoped file root. The explorer opens on a Volumes group (bind mounts and named Docker volumes) by default, with the stack source directory as a secondary group, on a "Files & Volumes" tab. - Discover roots from the rendered effective compose model; resolve named volumes to their Docker name and browse/edit them through the hardened helper container, with bind mounts handled directly when reachable. - Re-derive the allowed roots server-side on every file operation and match the client root id against them, so a request can never address a path the stack did not declare. Block dangerous host mounts and binds that overlap Sencho's managed directories; reject writes to read-only mounts. - Thread an optional root id through the existing file endpoints and an opaque, parseable optimistic-concurrency token through read, conflict, and write, for both filesystem and helper backends. - Keep compose and env file protection on the stack source root only. * fix(stacks): theme the Files & Volumes root switcher Replace the raw native select in the file-root switcher with the design system Select component. The native control did not honour the dark theme, so the panel rendered white with unreadable text. The themed Select gives a dark popover with grouped Volumes / Stack source labels and disabled items. * fix(stacks): contain the bind-root probe and de-taint the file-op error log Gate the volume-root bind probe's realpath/stat behind a compose-base containment check (mirroring the storage host-path probe) so they never run on an unvalidated host path; a source outside the compose dir is unreachable in the containerized deployment anyway and is reported non-accessible without touching the filesystem. Log the helper-backed file-op failure through a constant format string with sanitized arguments instead of an interpolated template literal. * fix(stacks): inline the bind-probe containment guard at the fs sinks The wrapped containment predicate was not recognized as a path barrier, so the bind probe's realpath/stat still flagged as uncontrolled-data-in-path. Inline the path.resolve + startsWith check directly at each filesystem sink (and re-check the resolved canonical before stat, so a within-base symlink that resolves outside the compose dir is also rejected). * fix(stacks): harden file-root lifecycle, upload race, and helper errors Address review findings on the Files & Volumes feature: - Invalidate the file-root allowlist on stack create/delete/import/from-git (wire StackFileRootsService.invalidateNode into invalidateNodeCaches), so a stack deleted and recreated under the same name cannot serve the old stack's roots from the 15s cache. - Use the atomic exclusive write for a non-overwrite upload so a file created by another writer after the existence check is not silently clobbered. - Let the helper's real cd errno through and map permission failures to 403 consistently across list/stat/read/write/mkdir/delete/pathKind, instead of reporting EACCES as 404/500; pathKind no longer reports a permission-denied parent as absent. - Document the realpath-then-open TOCTOU as a known, pre-existing limitation of every file op (O_NOFOLLOW is not viable because config volumes legitimately contain symlinks); the bind root is contained to the compose dir and the op requires stack:edit. - Docs: drop a missing screenshot reference and correct the protected-file delete behavior (stack-root compose/.env cannot be deleted via the explorer). |
||
|
|
b611f41872 |
fix(drift): stop flagging declared external networks as drift (#1402)
The spatial drift engine reported a service attached to a declared
external network (top-level networks: { foo: { external: true } }) as
"attached to a network not declared in compose", marking the stack
permanently drifted. runtimeResourceName project-prefixed every declared
network to <project>_<key>, but Docker never prefixes an external
network: it references a pre-existing network by its real name. The
phantom <project>_<key> never matched the runtime name, so the
attachment fell through to a foreign-network finding.
Resolve external networks to their real name (the key, or a name:
override) without the project prefix, so the raw declared adapter agrees
with the rendered model the Network Inspector already used. Add unit,
adapter, and engine-level regression tests, and extend the adapter
equivalence test to cover an external network with no name override.
|
||
|
|
b9314eb67f |
chore(deps): bump bundled containerd to v2.2.5 to clear CVE-2026-53488/53489/53492 (#1401)
Three newly published HIGH containerd advisories (CVE-2026-53488, CVE-2026-53489, CVE-2026-53492) affect github.com/containerd/containerd/v2 v2.2.4, which the compose-builder stage pins via go get. They are CRI checkpoint/restore and restart-monitor issues, fixed in v2.2.5. Bump the pin from v2.2.4 to v2.2.5 so the bundled docker-compose binary scans clean again. The vulnerable code paths are daemon-side (containerd's CRI service) and are not reached by compose, so this stays defense-in-depth. This is a build-time dependency bump with no product behavior change, so it is typed chore (no release-please bump, no user-facing changelog entry). |
||
|
|
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.
|
||
|
|
d26ab58189 |
feat(fleet): cross-node bulk label assign with authoritative label discovery (#1389)
* feat(fleet): cross-node bulk label assign with authoritative label discovery Make Fleet Actions > Bulk label assign work across the fleet. Pick a stack label that exists anywhere in the fleet, select stacks on one or more nodes, and the control orchestrates: each target node resolves the label by name, creating it with the same name and color if missing, then adds it to the selected stacks while preserving their existing labels. The local node runs in process; each remote runs its own admin-only local-assign receiver over the node proxy. Per-node failures (unknown node, no proxy target, unreachable, mixed-version remote) degrade that node only and are reported per node in the result. Assignment writes use a transactional INSERT OR IGNORE so the add-preserve path is idempotent and race-free. Also make the shared fleet label discovery authoritative: suggestions, match-preview, and the fleet-stop remote leg now read each node's labels live over the proxy instead of the control database, which does not mirror remote labels. A propagated label therefore appears in, and is stoppable by, Stop-by-label across the fleet, and unreachable nodes are surfaced rather than silently dropped. Fleet Actions runs against the unfiltered node list, so overview filters no longer narrow its scope. The previous node-scoped, replace-by-id bulk-assign endpoint is removed. * fix(fleet): treat malformed remote label responses as per-node failures A 200 response from a remote node whose body is not the expected shape was treated as a benign empty result, so a malformed remote could read as a clean zero-stack assign or a "matched, nothing to stop" no-op and even surface a success toast. Validate the wire shape in the bulk-assign and fleet-stop remote legs and in the authoritative label discovery fan-out; on a malformed body, report the node as a per-node failure with the error attributed to its stacks instead of silently dropping it. * chore: drop accidentally committed temp file |
||
|
|
cd9247db1e |
docs: retire the public-beta label and document v0.92.0 capabilities (#1398)
Sencho exits public beta after a clean v0.92.0 QA pass. Replace the global "public beta on the path to v1.0" callout with scoped maturity language across the README, KNOWN_LIMITATIONS, quickstart, troubleshooting, upgrade, and the security-architecture reference. The copy stays honestly pre-1.0 and avoids a blanket "stable" claim. Reframe the Scale limitations to credit QA, audit, and end-to-end validation while keeping the honest caveat that large-scale performance is not yet benchmarked. Fold the new v0.92.0 capabilities into the README sections: Compose Doctor preflight checks, health-gated updates with stalled-update recovery, file move/rename in the explorer, ordered multi-file Compose for Git sources, the Security overview, on-demand node-wide scans, the Compose network inspector with an exposure-intent guard, scan policy packs, documentation-drift flags, and configurable image-update cadence with registry/source links. |
||
|
|
6b63a67a0c |
ci: retry E2E tests in CI to absorb flaky real-Docker timing (#1396)
The E2E suite includes real-Docker deploy and health-gate specs whose timing varies on shared CI runners (cold image pulls, gate observation windows). With no retries configured, a single transient timing blip failed the whole Playwright job. Enable 2 retries in CI only; local runs stay at 0 for fast, deterministic feedback. The existing trace: 'on-first-retry' now captures the retried attempt for triage. |
||
|
|
ce9e1bca7e |
chore(main): release 0.92.0 (#1350)
Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com>v0.92.0 |
||
|
|
de87c41b78 | fix(updates): allow "Apply now" on the readiness board without a schedule (#1394) | ||
|
|
29c599d370 |
fix(stacks): return a handled response when saving env to a stack with no env file (#1393)
PUT /api/stacks/:stackName/env resolved no env path when a stack had a compose file but no .env on disk. It then wrote to an undefined path, which threw and surfaced as an opaque 500. Guard the missing-env case and return a clean 404 so direct API callers get an actionable response instead of a crash. The editor still only edits an existing env file, so behavior in the UI is unchanged. |
||
|
|
2821e87d3f |
fix: gate fleet stop-by-label on a resolved preview and validate remote stop responses (#1392)
* fix: gate fleet stop-by-label on a resolved preview and validate remote stop responses The destructive "Stop fleet by label" action could run before its blast radius was known, and a malformed remote response could be rendered as a successful zero-stack stop. Both are release blockers for the fleet stop-by-label flow. Gate the "Stop fleet" button on a resolved blast radius: the live match-preview resolving to at least one matching stack, or a dry run of the current label when the preview endpoint is unavailable. Carry the resolved node and stack list into the confirm modal so the operator confirms against the concrete targets rather than a label name alone. Editing the label invalidates a prior dry-run snapshot, so a stale blast radius cannot re-enable the action. Validate the remote local-stop 200 body before trusting it. A body that is not the local-stop contract (missing matched flag, non-array results, or a malformed result element) now fails that node with a clear error, consistent with the non-ok and unreachable paths, instead of defaulting matched to true and results to empty. * fix: ignore stale fleet stop-by-label preview responses after the label changes The debounced match-preview callback set the preview state unconditionally, so a request issued for one label that resolved after the operator switched to another label would mark the preview ready with the old label's blast radius. That re-enabled the destructive Stop and showed stale nodes/stacks in the confirm modal for a label that no longer matched them. Guard the in-flight request with a per-run cancelled flag flipped in the effect cleanup, so a response for a label that is no longer current cannot set the preview. This mirrors the cancellation pattern already used by the suggestions effect. Add a test that holds a preview request in-flight, switches the label, then resolves the stale response and asserts Stop stays disabled and the old targets do not leak. |
||
|
|
ba09e6f69e |
fix: base Git multi-file Compose deploy env and dossier on the effective config (#1391)
* fix: resolve the root .env at deploy and render time for Git context-dir stacks A Git multi-file source with a context dir set --project-directory to that dir, so Docker Compose looked for .env there and missed the root .env Sencho writes. Validation already passed the root .env with --env-file, so a stack could validate with one effective config but deploy or render another. Add authoredComposeEnvFileArgs, which appends --env-file <stackDir>/.env when the applied deploy spec has a context dir and a root .env exists, and wire it into the deploy/update, image-scan, render, and container-listing compose invocations so they all resolve env from the same file the validator used. A non-ENOENT access error surfaces instead of silently dropping the flag. * fix: base multi-file Git dossier and doc-drift on the effective Compose model The Stack Dossier and its documentation-drift check parsed only the stored root compose file. For a multi-file Git source, services, ports, networks, or volumes that an override file adds were invisible, so the dossier showed incomplete facts and doc-drift could falsely warn that a documented port is unpublished when an override actually publishes it. Add a secret-safe GET /stacks/:name/effective-anatomy that renders the merged effective model and extracts only structural facts (services, ports, volumes, networks, restart), never env, label, or command values. StackAnatomyPanel fetches it for multi-file Git stacks and feeds those facts into the dossier and doc-drift, falling back to the root-only parse for single-file or non-git stacks and whenever the render is unavailable. * fix: add an inline path-injection barrier to the Git env-file resolver CodeQL js/path-injection flagged the fs.access in authoredComposeEnvFileArgs because the env path derives from the route-supplied stack name and the only containment check lived in the callers, not at the sink. Resolve the stack dir against the compose base and assert containment with startsWith inline, then derive the .env path from the validated dir, mirroring the existing inline guards in renderConfig and validateCompose. Valid stack names are unaffected; a name that escapes the base now yields no --env-file. * test: stabilize the dossier doc-drift e2e against the dossier-load race The first assertion filled the access_urls field as soon as the Dossier panel was visible, but the panel's GET /stacks/:name/dossier resolves by overwriting the fields from the server (empty access_urls) and only then flips the doc-drift gate on. When the GET landed after the fill, it clobbered the typed value and the warning never rendered, so the test failed intermittently under CI timing. Wait for that GET to land before typing, mirroring the spec's openStack helper. |
||
|
|
5f1baa7522 |
fix: harden deploy/update concurrency and node-targeting safety (#1390)
* fix: harden deploy/update concurrency and node-targeting safety Release stabilization for deploy/update operational safety. Per-stack operation locking is now global. Background lifecycle paths (scheduler auto stop/down/start/backup/update, webhook execute, Git source auto-deploy, image auto-update, label bulk actions, fleet snapshot redeploy, and mesh redeploy) acquire the per-node, per-stack lock through a new StackOpLockService.runExclusive helper and skip rather than race a manual deploy/update/rollback/backup on the same stack and node. Skips surface honestly (a failed scheduled run, a recorded webhook failure, a per-stack batch result, or a thrown error) instead of a silent no-op. Update readiness and policy-bypass now run against the node captured when the dialog opened, not the live active node, so switching nodes while a dialog is open cannot retarget the update or the bypass retry. Rollback readiness no longer presents a moving-tag or unpinned image as a ready image revert. Restoring files does not revert a moving tag, so those stacks read as partial, and the rollback success message states that the compose and env files were restored. * fix: lock blueprint reconcile against manual ops and correct rollback wording Follow-up to the deploy/update safety hardening, closing two more gaps from a verification pass. BlueprintService.deployLocal and withdrawLocal called ComposeService directly, so blueprint reconciliation could race a manual deploy/update/rollback/backup on an owned stack. Both now run their compose lifecycle call through StackOpLockService.runExclusive and skip (recorded as a failed reconcile, retried on the next cycle) on conflict. The withdraw holds the lock across both the compose down and the directory delete so neither races a manual operation. The runtime rollback messages overstated recovery: a rollback restores the compose and env files and recreates containers, but does not revert an image behind a moving tag. The auto-rollback deploy-progress output, the recovery panel and chip, the failure toasts, and the manual rollback route message now state that the compose and env files were restored, with the matching OpenAPI example and atomic-deployments doc updated. * fix: acquire stack lock before blueprint deploy mutates compose and marker files Local blueprint deploy wrote the compose and marker files and ran the policy assert before acquiring the per-stack lock; the lock only wrapped the deploy itself. A reconcile could therefore rewrite an owned stack's files while a manual deploy/update/rollback/backup was running. The lock now wraps the whole critical section (create, write compose, write marker, policy assert, deploy), so on conflict nothing is written and the reconcile records a failed outcome. Adds a test asserting a deploy under a held lock records failed, writes no marker file, and leaves the manual lock untouched. * fix: make remote blueprint apply atomic under the receiving node's stack lock Remote blueprint deploy wrote the compose and marker files to the target node via separate HTTP calls and only locked on the final deploy, so the file writes could race a manual operation on that node. A node's operation lock is process-local and cannot be held by the hub across HTTP calls, so the locked create/write/deploy now runs on the receiving node. The locked critical section is extracted into BlueprintService.applyLocalUnderLock and exposed via POST /api/blueprints/apply-local. The hub posts the blueprint to that endpoint in one call; the receiving node runs create + write compose+marker + deploy under its own per-stack lock. Older nodes without the route answer 404 and fall back to the legacy multi-call flow. The endpoint is gated by paid tier and the same per-stack stack:edit and stack:deploy permissions as the PUT-compose + deploy it bundles, validates the stack name, compose size, and marker structure, and returns 409 on a lock conflict without writing anything. Adds tests for the atomic single-call path, the 404 legacy fallback, the 409 lock-conflict mapping, the route validation and permission paths, and the write-compose-then-marker-then-deploy ordering of the shared locked apply. * fix(deps): bump undici to 7.28.0 to clear high-severity advisory The frontend CI npm audit gate (--audit-level=high) failed on a transitive undici 7.25.0 (a dev-only dependency via jsdom): TLS certificate validation bypass (GHSA-vmh5-mc38-953g) and cross-user cache information disclosure (GHSA-pr7r-676h-xcf6). Bumping undici within jsdom's existing ^7.25.0 range to 7.28.0 clears the high-severity advisory and unblocks the frontend job. Lockfile only; no direct dependency or source change. |
||
|
|
2960f9f853 |
fix(sidebar): skip the auto-update next-run poll for non-admins (#1388)
The sidebar next-run indicator polled GET /scheduled-tasks on mount, on a 60s interval, and on every state-invalidate event. That route is admin-only, so non-admin users hit a 403 on every poll, leaving a steady stream of resource errors in the browser console with no functional purpose. Gate the poll on the admin role read from useAuth(): when the user is not an admin, skip the fetch, interval, and listener entirely and report no scheduled run. The effect re-evaluates on role change, so polling starts or stops cleanly on login, logout, or a role update. |
||
|
|
c0f84cb04b |
chore(deps): bump the all-npm-backend group in /backend with 11 updates (#1387)
Bumps the all-npm-backend group in /backend with 11 updates: | Package | From | To | | --- | --- | --- | | [axios](https://github.com/axios/axios) | `1.17.0` | `1.18.0` | | [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | `12.10.0` | `12.11.1` | | [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) | `4.1.0` | `4.1.1` | | [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) | `1.38.4` | `1.38.5` | | [multer](https://github.com/expressjs/multer) | `2.1.1` | `2.2.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `25.9.3` | | [eslint](https://github.com/eslint/eslint) | `10.4.1` | `10.5.0` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.0` | `8.61.1` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` | | [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1065.0` | `3.1070.0` | | [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1065.0` | `3.1070.0` | Updates `axios` from 1.17.0 to 1.18.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.17.0...v1.18.0) Updates `better-sqlite3` from 12.10.0 to 12.11.1 - [Release notes](https://github.com/WiseLibs/better-sqlite3/releases) - [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v12.10.0...v12.11.1) Updates `http-proxy-middleware` from 4.1.0 to 4.1.1 - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/master/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v4.1.0...v4.1.1) Updates `isomorphic-git` from 1.38.4 to 1.38.5 - [Release notes](https://github.com/isomorphic-git/isomorphic-git/releases) - [Commits](https://github.com/isomorphic-git/isomorphic-git/compare/v1.38.4...v1.38.5) Updates `multer` from 2.1.1 to 2.2.0 - [Release notes](https://github.com/expressjs/multer/releases) - [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md) - [Commits](https://github.com/expressjs/multer/compare/v2.1.1...v2.2.0) Updates `@types/node` from 25.9.2 to 25.9.3 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `eslint` from 10.4.1 to 10.5.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0) Updates `typescript-eslint` from 8.61.0 to 8.61.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.61.1/packages/typescript-eslint) Updates `vitest` from 4.1.8 to 4.1.9 - [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.9/packages/vitest) Updates `@aws-sdk/client-ecr` from 3.1065.0 to 3.1070.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.1070.0/clients/client-ecr) Updates `@aws-sdk/client-s3` from 3.1065.0 to 3.1070.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.1070.0/clients/client-s3) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: better-sqlite3 dependency-version: 12.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: http-proxy-middleware dependency-version: 4.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: isomorphic-git dependency-version: 1.38.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: multer dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: "@types/node" dependency-version: 25.9.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: eslint dependency-version: 10.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: typescript-eslint dependency-version: 8.61.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: vitest dependency-version: 4.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: "@aws-sdk/client-ecr" dependency-version: 3.1070.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.1070.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> |
||
|
|
af6c39a716 |
chore(deps): bump the all-npm-frontend group (#1386)
Bumps the all-npm-frontend group in /frontend with 24 updates: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.16` | `1.1.17` | | [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.4` | `1.3.5` | | [@radix-ui/react-context-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/context-menu) | `2.3.0` | `2.3.1` | | [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.16` | `1.1.17` | | [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.17` | `2.1.18` | | [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/hover-card) | `1.1.16` | `1.1.17` | | [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.9` | `2.1.10` | | [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.16` | `1.1.17` | | [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.11` | `1.2.12` | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.0` | `2.3.1` | | [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.9` | `1.1.10` | | [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.0` | `1.4.1` | | [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.5` | `1.3.0` | | [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.14` | `1.1.15` | | [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.9` | `1.2.10` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.17.0` | `1.20.0` | | [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.5.0` | `1.6.0` | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.0` | `4.3.1` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `25.9.3` | | [eslint](https://github.com/eslint/eslint) | `10.4.1` | `10.5.0` | | [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.2` | `0.5.3` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.1` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.0` | `8.61.1` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` | Updates `@radix-ui/react-alert-dialog` from 1.1.16 to 1.1.17 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog) Updates `@radix-ui/react-checkbox` from 1.3.4 to 1.3.5 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox) Updates `@radix-ui/react-context-menu` from 2.3.0 to 2.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/context-menu/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/context-menu) Updates `@radix-ui/react-dialog` from 1.1.16 to 1.1.17 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog) Updates `@radix-ui/react-dropdown-menu` from 2.1.17 to 2.1.18 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu) Updates `@radix-ui/react-hover-card` from 1.1.16 to 1.1.17 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/hover-card/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/hover-card) Updates `@radix-ui/react-label` from 2.1.9 to 2.1.10 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label) Updates `@radix-ui/react-popover` from 1.1.16 to 1.1.17 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover) Updates `@radix-ui/react-scroll-area` from 1.2.11 to 1.2.12 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area) Updates `@radix-ui/react-select` from 2.3.0 to 2.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-separator` from 1.1.9 to 1.1.10 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator) Updates `@radix-ui/react-slider` from 1.4.0 to 1.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider) Updates `@radix-ui/react-slot` from 1.2.5 to 1.3.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot) Updates `@radix-ui/react-tabs` from 1.1.14 to 1.1.15 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs) Updates `@radix-ui/react-tooltip` from 1.2.9 to 1.2.10 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip) Updates `lucide-react` from 1.17.0 to 1.20.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.20.0/packages/lucide-react) Updates `radix-ui` from 1.5.0 to 1.6.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radix-ui) Updates `@tailwindcss/vite` from 4.3.0 to 4.3.1 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite) Updates `@types/node` from 25.9.2 to 25.9.3 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `eslint` from 10.4.1 to 10.5.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0) Updates `eslint-plugin-react-refresh` from 0.5.2 to 0.5.3 - [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases) - [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md) - [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.2...v0.5.3) Updates `tailwindcss` from 4.3.0 to 4.3.1 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss) Updates `typescript-eslint` from 8.61.0 to 8.61.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.61.1/packages/typescript-eslint) Updates `vitest` from 4.1.8 to 4.1.9 - [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.9/packages/vitest) --- updated-dependencies: - dependency-name: "@radix-ui/react-alert-dialog" dependency-version: 1.1.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-checkbox" dependency-version: 1.3.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-context-menu" dependency-version: 2.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-dialog" dependency-version: 1.1.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-dropdown-menu" dependency-version: 2.1.18 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-hover-card" dependency-version: 1.1.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-label" dependency-version: 2.1.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-popover" dependency-version: 1.1.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-scroll-area" dependency-version: 1.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-separator" dependency-version: 1.1.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-slider" dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-slot" dependency-version: 1.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-tabs" dependency-version: 1.1.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-tooltip" dependency-version: 1.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: lucide-react dependency-version: 1.20.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: radix-ui dependency-version: 1.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@tailwindcss/vite" dependency-version: 4.3.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@types/node" dependency-version: 25.9.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: eslint dependency-version: 10.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: eslint-plugin-react-refresh dependency-version: 0.5.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: tailwindcss dependency-version: 4.3.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: typescript-eslint dependency-version: 8.61.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: vitest dependency-version: 4.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
465890bcac |
chore(deps-dev): bump @playwright/test in the all-npm-root group (#1385)
Bumps the all-npm-root group with 1 update: [@playwright/test](https://github.com/microsoft/playwright). Updates `@playwright/test` from 1.60.0 to 1.61.0 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.61.0 dependency-type: direct:development update-type: version-update:semver-minor 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> |
||
|
|
d364f6acc8 |
chore(deps): bump node from 144769e to 9c0e1e5 (#1384)
Bumps node from `144769e` to `9c0e1e5`. --- updated-dependencies: - dependency-name: node dependency-version: 26-alpine dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
fca659c815 |
chore(deps): bump golang from f23e8b2 to f1ddd9f (#1383)
Bumps golang from `f23e8b2` to `f1ddd9f`. --- 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> |
||
|
|
cb58cc423f |
fix(fleet): resolve Stop-by-label stack labels across all nodes (#1382)
* fix(fleet): resolve Stop-by-label stack labels across all nodes The Fleet Actions "Stop by label" card only ever saw the control node's own stack labels. The stack-label routes are proxied, so each node stores its labels in its own database and the control holds no mirror for remote nodes. The suggestions, match-preview, and fleet-stop endpoints all read that nonexistent mirror, so remote-only labels were invisible and remote stacks were skipped before the remote was ever asked. Make all three authoritative across the fleet with a new collectFleetLabelSummaries helper: the local node reads its own database, and each remote is queried live through its labels and label-assignments endpoints over the proxy, with fail-closed parsing and per-node reachability. fleet-stop drops the mirror pre-check and always calls each reachable remote's local-stop receiver, reporting unreachable nodes at the node level so they never block the reachable ones. The picker now surfaces remote-only labels, aggregates shared names once with combined counts and the carrying node names, and flags incomplete coverage when a node is unreachable. The preview groups matches per node, lists unreachable nodes separately, and distinguishes no matching stacks from "label exists but no stacks" from "remote unavailable". * fix(fleet): harden Stop-by-label card against malformed responses Guard the match-preview and fleet-stop response bodies so a malformed but 200 reply degrades instead of crashing or misreporting. match-preview now validates the per-node shape before rendering (the preview reads it outside any try/catch) and logs a malformed body; fleet-stop distinguishes a non-array results body (a server bug, now logged and surfaced as an unexpected-response error) from a genuine empty fleet, and guards per-node stackResults. Docs: an unreachable or errored remote is reported once per node, not as a per-stack error row. |
||
|
|
f166a537c3 |
feat(fleet): reorder Overview toolbar actions so Add node sits far right (#1381)
Swap the trailing action buttons in the Fleet Overview toolbar so Check Updates sits between the view-mode toggle and Add node, with Add node anchored at the far right edge. |
||
|
|
f23b7e1bac |
feat: ordered multi-file Compose for Git sources (#1380)
* feat: ordered multi-file Compose for Git sources
Extend Git sources to deploy an ordered list of compose files merged with
docker compose -f base.yaml -f override.yaml ..., plus an optional project
directory.
- Pick and reorder compose files from the repository tree (drag to reorder on
desktop, up/down arrows on phones); manual path entry is also supported.
- The ordered set drives every stack-scoped compose command (deploy, update,
start/stop/restart/down, image scans, Compose Doctor) and the container
lookup, so a service or image declared only in an override is handled too.
- Runtime keys off the materialized set, not the saved configuration: saving a
source does not change deploy args until the pull is applied, and apply
materializes from the pending snapshot rather than live config.
- The project directory is passed as --project-directory, with -p <stack>
pinning the Compose project so container labels stay stable.
- The Mesh override is layered last; single-file sources are byte-identical to
before, and existing rows keep working via the single-path fallback.
Docs cover the picker, ordering, project directory, and the new troubleshooting
and limitations (referenced files are not materialized; the dependency graph,
drift, and networking views read the primary file).
* fix: harden multi-file Git source (hash, unlink, collisions, node id)
- hashContent folds ordered file CONTENTS (not paths) so a clean multi-file
stack is not flagged as locally edited: create/apply hash the fetched files
(repo paths) while pull hashes the on-disk files (materialized paths), which
previously disagreed and showed a false "local edits detected".
- Block unlinking a multi-file or project-directory Git source (409): the deploy
spec lives on the source row, so removing it would silently revert deploys to
root compose.yaml. Single-file sources still unlink.
- Reject materialized-path collisions in the selection validator: an additional
file equal to or nested under compose.yaml, an ancestor/descendant overlap
between selected files, and a project directory nested under a compose file
(previously a 500 at materialization).
- DockerController.getContainersByStack uses the controller's node compose dir
and passes its node id to the authored prefix, instead of the process default.
* fix: CI failures on multi-file Git source (test crash, aria query, path barrier)
- GitSourceFields no longer crashes when repoUrl/branch are falsy: the canBrowse
trim() is optional-chained, so a reusable field component tolerates partial
props. Fixes the apply-binding panel test, which feeds a minimal source object.
- GitSourcePanel tests query the footer Remove button by its exact name, so the
picker's per-file "Remove <path>" buttons no longer collide with the broad
/remove/i match (the test intent, footer Remove present/absent, is unchanged).
- validateCompose uses an inline resolve + startsWith barrier at the context-dir
mkdir sink (CodeQL does not credit the wrapped isPathWithinBase helper),
clearing the js/path-injection alert. The containment check is equivalent and
contextDir is also validated upstream.
* test: update Git source E2E spec for the multi-file compose picker
The compose-file picker replaced the single #git-source-path input and added
per-file Remove buttons, so the E2E spec drove selectors that no longer exist:
- Drop the redundant compose.yaml fills (the picker defaults to compose.yaml).
- Select the footer Remove button by exact name so the picker's per-file
"Remove <path>" buttons no longer make the locator ambiguous.
- Set a custom compose path through the picker (add via the manual input, press
Enter, then remove the default compose.yaml).
* test: match the footer Remove button with an exact Playwright name
Playwright's getByRole name option is a substring match by default, so
{ name: 'Remove' } also matched the picker's "Remove <path>" buttons. Require an
exact match so only the footer Remove button is selected.
|
||
|
|
7ce045accb |
feat: pre-deploy scan visibility and pinned scanner version (#1378)
* feat: pre-deploy scan visibility and pinned scanner version Pin managed Trivy installs and add an opt-in pre-deploy scan advisory so a manual deploy can surface each image's latest scan before it runs. - Managed Trivy now installs a pinned, known-good version by default for reproducible installs. Auto-update still tracks the latest release, and an explicit update always pulls the latest. - Add an opt-in pre-deploy scan advisory: when enabled, deploying a stack from the editor first shows each image's latest cached scan severity for review. It is visibility only and never blocks; deploy enforcement is unchanged. - Backend: pre_deploy_scan_advisory setting, PUT /security/pre-deploy-scan-advisory, a cache-only GET /security/stacks/:name/pre-deploy-summary, and a node-scoped getLatestVulnScanByDigestForNode lookup. - Frontend: advisory toggle on the Security page scanner setup, and a PreDeployScanDialog wired into the editor deploy flow that fails open when the summary is unavailable. - Docs: scanner configuration, version pinning, and the advisory. * fix: harden pre-deploy advisory guard, toggle visibility, and installer busy state Addresses review findings on the pre-deploy advisory. - Block a second editor deploy during the async advisory window with a synchronous pending ref, cleared on cancel and in the deploy's finally, so a double-click can no longer start two deploys. - Keep the pre-deploy advisory toggle visible to admins whenever the setting is on, so it can still be turned off after the scanner becomes unavailable. - Resolve the managed Trivy version inside the install lock so the busy state and serialization cover the latest-version fetch and the managed-install check. |
||
|
|
770bead889 |
fix(deps): bump form-data, protobufjs, and vite to clear high-severity CVEs (#1379)
The npm audit and Trivy image-scan CI gates fail on newly disclosed high-severity advisories in transitive backend dependencies. Bump the locked versions to their fixed releases: - form-data 4.0.5 -> 4.0.6 (via axios; CVE-2026-12143, CRLF injection) - protobufjs 7.5.8 -> 7.6.4 (via dockerode/@grpc; CVE-2026-48712, Any-expansion DoS) - vite 8.0.5 -> 8.0.16 (vitest dev dependency; fs.deny bypass and launch-editor) Lockfile-only change (npm audit fix, no package.json edits). Backend build, lint, and the route/integration tests stay green, and backend npm audit --audit-level=high now reports 0 vulnerabilities. |
||
|
|
058cf8f2c7 |
feat: make image-update check cadence configurable and visible (#1377)
* feat: make image-update check cadence configurable and visible The background image-update scanner polled registries on a hardcoded 6-hour interval, with no way to see when it last ran or when the next run was due. Operators testing updates read this as auto-update being unreliable: a manual update checks the registry immediately and applies, so the slow background scan rarely raised the "update available" notification before the stack was already current. Backend: - ImageUpdateService reads image_update_check_interval_minutes (15-1440, default 120) and drives a single generation-guarded self-rescheduling timer with 10% per-run jitter so fleet nodes do not poll in lockstep. restartPolling() applies a new interval live, with no restart, and cannot leave a duplicate timer when a save lands mid-scan. - GET /api/image-updates/status now returns checking, intervalMinutes, lastCheckedAt, nextCheckAt, and the manual-cooldown fields. New admin-only PUT /api/image-updates/interval persists the setting and reschedules. Frontend: - New Settings > Automation > Image update checks section to choose the interval (read-only for non-admins; admin enforced on the backend). - The Auto-Update readiness view shows last-checked, next-check, and a ticking manual-recheck cooldown, and the copy distinguishes registry detection from scheduled auto-update execution. Adds backend unit and route tests and frontend component tests, and updates the auto-update documentation. * fix: drop stale image-update status response in the readiness strip loadCadence() ran on mount and again after a Recheck with no request token, so a slow initial /image-updates/status response could resolve after the recheck-triggered one and overwrite the fresh cooldown with stale data, or set state after the view unmounted. Guard setCadence with a monotonic token mirroring loadReadiness, and bump it on unmount. Adds a regression test for the out-of-order resolution. |
||
|
|
02c3b006eb |
feat(fleet): refine the Fleet Overview toolbar (#1376)
* feat(fleet): tidy the Overview toolbar and shorten tab labels - Move the Add node button into the Overview toolbar beside the Grid/Topology toggle so it sits with the view it acts on instead of showing on every Fleet tab. It stays admin-only. - Collapse the node search to an icon button that expands to the full input on click and collapses again on blur once the query is empty, reclaiming toolbar width. An active query keeps it open. - Match the sort-direction toggle and the sort dropdown to the outlined dark fill of the Filters button for a consistent toolbar row. - Swap the Check Updates icon to the refresh-with-dot glyph. - Rename two tabs: Fleet Actions to Actions, Dependencies to Map (display labels only; internal keys unchanged). Update the feature docs to the new tab labels. * feat(fleet): move Check Updates into the Overview tab toolbar Check Updates is an Overview-specific action, but it lived in the shared Fleet header toolbar that renders across every tab. Move it into the Overview tab's own toolbar, to the right of the Add node button, so it only appears where it applies. The relocated button now spins and disables while a check is in flight, matching the Refresh button's loading feedback. The shared header keeps Refresh and Export Dossier. |
||
|
|
85bcd1341e |
fix(mobile): keep the bottom tab bar visible with dynamic viewport height (#1375)
On mobile browsers that place the address bar at the bottom (Safari, Chrome), 100vh measures the largest viewport with the bar retracted, so the mobile shell was taller than the visible area and the bottom tab bar sat behind the address bar. Switching the mobile shell container to h-dvh ties its height to the dynamic viewport, which tracks the visible area as the bar shows and hides and keeps the tab bar on screen. Desktop is untouched: only the mobile branch changed, and the tab bar already pads for the home-indicator safe area. |
||
|
|
6cc66faa8c |
feat(mobile): standardize secondary pages and the stack list on the status masthead (#1374)
* feat(security): reflow the node Security page for mobile Below the md breakpoint the Security page now reads as a phone surface instead of a squeezed desktop, with no change to the desktop layout. - Masthead stat cluster moves into a full-width 3-cell strip (critical / high / last scan) below the tab strip, since the masthead hides its inline cluster on a phone. - The eight-section tab strip becomes a horizontally scrollable mono row with an edge mask-fade and a cyan underline on the active tab; every section stays reachable by scroll. - The six totals render as a 3x2 hairline-divided grid instead of the 640px-wide rail that forced a horizontal scroll. - The Images tab becomes a filterable, scrollable list (severity dot, truncated ref, freshness, critical/high count tags) with a chip row, in place of the desktop table. - A freshness footer band states scan recency and scanner version. All mobile treatment is gated by useIsMobile() or max-md: utilities, so the desktop view is byte-identical. Charts are reused full-width. * feat(security): make the mobile Security page a bespoke masthead-led screen On a phone the Security page now drops the global top bar and leads with its masthead (the notifications + more-menu cluster moves into the masthead's right slot), matching Home and Fleet so the mobile shell is continuous across pages. The view is reclassified bespoke and rendered through the masthead-led path; the desktop layout is unchanged. The mobile "more" menu now lists every destination instead of omitting the bottom-tab views, so the same menu opens the same set on every screen rather than changing contents from page to page. * refactor(mobile): extract shared PageHead, sub-tabs, and chip-row primitives Add PageHead (the header for a pushed full-screen secondary view), MobileSubTabs (the mono tab scroller with the cyan active underline), and MobileChipRow (the cyan-filled filter chips) to the shared mobile-ui kit, and rewire the Security page's tab strip and Images filter to consume them. No visual change; this is the shared chrome the remaining mobile pages reuse. * feat(updates): make the mobile Updates page a bespoke masthead-led screen Below the md breakpoint the Auto-Update Readiness page becomes a pushed full-screen view: a PageHead (back chip, the rehomed notifications + more-menu, a "fleet readiness" crumb, and a Recheck action) leads, then a brand-tinted readiness hero, per-node sections, and one-up readiness cards that reuse the same risk badge, version delta, and apply/disabled logic as the desktop board. The desktop layout is unchanged. Reclassifies auto-updates as bespoke and renders it through renderMobileBespoke behind the same hub-only + capability gates as the desktop content path. Also lifts the PageHead, sub-tabs, and chip-row primitives' right-slot to host the rehomed global chrome. * feat(app-store): make the mobile App Store a bespoke masthead-led screen Below the md breakpoint the App Store becomes a pushed full-screen view: a PageHead (back chip, the rehomed notifications + more-menu, and an app-count crumb) leads, the category sidebar collapses into a horizontal chip scroller, and the featured hero plus the tile grid (already single-column on a phone) stack below. The featured hero, tile grid, and deploy sheet are shared with the desktop layout, which is unchanged. Reclassifies templates as bespoke and renders it through renderMobileBespoke. Adds tests for the shared chip row and sub-tabs. * feat(audit): make the mobile Audit Log a bespoke masthead-led screen Below the md breakpoint the Audit Log becomes a pushed full-screen view: a PageHead (back chip, the rehomed notifications + more-menu, an entry-count crumb, and Refresh) leads, then the Stream/Table sub-tabs and the stream view, where the signal-rail tiles fold to a 2x2 grid and the day-banded activity stream reflows. The columnar table reads best on a larger screen, so the Table tab points there on a phone. Desktop is unchanged. Reclassifies audit-log as bespoke behind the same hub-only + capability gates as the desktop content path. * feat(app-store): drop the featured hero and category chips on mobile On a phone the App Store is now search plus a single-column list of every matching app. The featured hero and the category chip row are removed; the would-be-featured app is folded into the list so nothing is dropped. Desktop keeps the featured hero, category sidebar, and grid. * feat(resources): make the mobile Resources page a bespoke masthead-led screen Below the md breakpoint Resources becomes a pushed full-screen view: a PageHead (back chip, the rehomed notifications + more-menu, a docker crumb) leads, then the reclaim hero, the disk-footprint segments, the 2x2 quick-clean grid, and the resource tabs. The raw resource tables scroll horizontally to fit; the detail sheets stay full-screen. The main content and the dialog/sheet overlays are shared with the desktop layout, which is unchanged. * chore(mobile): correct shared-primitive comments and dedupe the lazy fallback Fix the "shared by" consumer lists on the mobile-ui primitives, point the headerActions doc comments at the PageHead (not a masthead), drop the stale "all eight sections" count on the Security tab strip, rename a readiness-card test to match what it asserts, and extract a single Suspense fallback for the four bespoke phone-screen lazy imports. No behavior change. * feat(mobile): codify the fade+arrow tab scroller and fix the Resources tab clip Extract the horizontal tab scroller (edge fade + clickable chevron + wheel-to-horizontal) out of the stack anatomy panel into a shared ScrollableTabRow primitive, and adopt it in the stack anatomy tabs, the mobile sub-tabs (Security / Audit), and the Resources resource tabs. On a phone the Resources tabs now scroll horizontally, so the "Unmanaged" tab and its count no longer clip out of the frame. Desktop is unchanged. * feat(logs): make the mobile Logs page a bespoke masthead-led screen Below the md breakpoint the global Logs view drops the TopBar for a PageHead, hides the metrics rail, collapses the stream and level filters into a single Filters dropdown, turns the search into an icon that expands to an input, and folds the pause / clear / download controls into an expanding floating action button that retracts after each action. Desktop is unchanged. Reclassifies global-observability as bespoke behind the same hub-only gate as the desktop content path. * feat(mobile): standardize secondary pages on the status masthead Adopt the Home/Fleet/Security status masthead (cyan rail, kicker, serif- italic state word + tone dot, meta line, notifications + more-menu in the right slot) on every bespoke secondary page (Resources, App Store, Updates, Audit, Logs), replacing the title-led PageHead, which is removed. Each page derives a status word: Updates "Up to date" / "N pending", Logs Streaming / Idle / Offline, Audit Healthy / Review / Alerts, Resources Reclaimable / Tidy, App Store the app count. The "< Stacks" back chip is dropped (the bottom tab bar and more-menu own navigation), the page actions (Recheck, Refresh) move into the body, and the Updates readiness hero folds into the masthead. Also make the Resources tab tables scroll horizontally instead of clipping on the right. Desktop is unchanged. * feat(stacks): lead the mobile stack list with the status masthead On phones the stack list now opens with the shared status masthead instead of the global top bar plus an in-sidebar node row. The node switcher renders as a compact kicker chip in the masthead, the serif word summarizes stack health (down, updates, or all running), and notifications plus the more-menu sit in the right slot. This matches the Home, Fleet, and Security pages. The dropped top bar hosted global search, so the more-menu gains a Search item that opens the command palette. The masthead kicker now accepts either a styled kicker or a raw slot, enforced as a discriminated union so exactly one source is supplied. Desktop is unchanged: the new chrome is gated to the mobile shell and the sidebar rows hide via max-md only. * fix(stacks): only call the list "All running" when every stack is up The mobile stack masthead derived its health word from filterCounts, where up counts running stacks and down counts exited ones. Any other status, and the window before statuses load, counts as neither, so a list with no exited stacks but some not yet running fell through to "All running" even though it was not. Gate that label on up equal to all, and otherwise show the running count out of the total so the headline stays honest while statuses settle. |
||
|
|
888f658a7a |
feat(files): move files and folders across directories in the stack explorer (#1373)
* feat(files): move files and folders across directories in the stack explorer Add a cross-directory move to the stack file explorer. Files and folders can be relocated either through a "Move to..." context-menu item that opens a folder-picker dialog, or by dragging an entry onto a folder node (or onto the root area to move it to the stack root). The backend reuses the existing rename endpoint: renameStackPath now resolves both ends through the leaf helper, so a symlink moves as the link entry rather than its target, and it guards against moving a directory into its own subtree. A cross-filesystem rename surfaces as a clean 409 instead of a 500. Protected root files (compose / docker-compose / .env) stay put. Moving the open file, or a folder containing it, deselects the viewer; a move that would discard unsaved edits is blocked with a clear message. * fix(files): fold case in move guards and keep the move dialog open on failure Harden the cross-directory move against case-insensitive filesystems and fix a dialog dismissal edge: - Protected root files (compose / docker-compose / .env) were gated by an exact, lowercase name match. On a case-insensitive filesystem a request like COMPOSE.YAML resolves to the real compose.yaml and slipped past the gate, so a protected file could be moved out of the stack root via the API. The gate now folds case on case-insensitive platforms; Linux stays case-sensitive, where a differently-cased name is a distinct, unprotected file. - The directory-into-descendant guard compared resolved paths case-sensitively, so a source supplied with non-disk casing skipped the guard and fell through to an opaque OS error (500) instead of a clean 400. The comparison now folds case the same way. - The move dialog closed after awaiting the move regardless of outcome, so a blocked move (unsaved edits) or a failed move dismissed the picker as if it had succeeded. The shared handler now reports success and the dialog only closes on an actual move. |
||
|
|
0066887cee |
feat(security): reflow the node Security page for mobile (#1372)
* feat(security): reflow the node Security page for mobile Below the md breakpoint the Security page now reads as a phone surface instead of a squeezed desktop, with no change to the desktop layout. - Masthead stat cluster moves into a full-width 3-cell strip (critical / high / last scan) below the tab strip, since the masthead hides its inline cluster on a phone. - The eight-section tab strip becomes a horizontally scrollable mono row with an edge mask-fade and a cyan underline on the active tab; every section stays reachable by scroll. - The six totals render as a 3x2 hairline-divided grid instead of the 640px-wide rail that forced a horizontal scroll. - The Images tab becomes a filterable, scrollable list (severity dot, truncated ref, freshness, critical/high count tags) with a chip row, in place of the desktop table. - A freshness footer band states scan recency and scanner version. All mobile treatment is gated by useIsMobile() or max-md: utilities, so the desktop view is byte-identical. Charts are reused full-width. * feat(security): make the mobile Security page a bespoke masthead-led screen On a phone the Security page now drops the global top bar and leads with its masthead (the notifications + more-menu cluster moves into the masthead's right slot), matching Home and Fleet so the mobile shell is continuous across pages. The view is reclassified bespoke and rendered through the masthead-led path; the desktop layout is unchanged. The mobile "more" menu now lists every destination instead of omitting the bottom-tab views, so the same menu opens the same set on every screen rather than changing contents from page to page. |
||
|
|
a5109e7916 |
feat(editor): enable mobile compose and env editing (#1371)
* feat(editor): enable mobile compose and env editing The mobile stack-detail Compose segment was read-only and told users to edit on desktop. Operators need to make small emergency edits from a phone, so the Compose segment now opens a full-screen editor for small, safe compose and .env changes. The editor is a lightweight monospace textarea rather than Monaco, sized for small corrections at common phone widths. It reuses the existing desktop save path from useStackActions and the global overlays, so every protection behaves the same: ETag conflict handling, diff preview when enabled, save-only, save-and-deploy, and the unsaved-changes guard. A compose/.env toggle appears when the stack has an env file, and the env-file picker is locked while edits are unsaved so switching files cannot drop them. Editing is gated by the same stack:edit permission as desktop. A footer note reminds users that mobile editing is for small changes and points large rewrites to desktop. The desktop Monaco editor is unchanged. * fix(editor): keep the mobile editor save target in sync with the shown buffer Two edge cases in the mobile compose/.env editor could silently drop an edit: - When the desktop editor was on the Files tab (or an env tab with no env file) and the viewport crossed into the mobile breakpoint, the editor showed the compose buffer while the shared active tab stayed on files, so a save quietly no-opped. Normalize the active tab to compose on the mobile surface so the visible edit always saves to the visible file. - The textarea stayed writable while an env-file switch was loading, so edits typed during the fetch were overwritten when it resolved. Make the textarea read-only while a file load is in flight. Adds unit tests for both normalizations and the read-only-during-load guard. |
||
|
|
49f1b49ac6 |
fix(settings): clear stale pending and unsaved indicators after save (#1370)
* fix(settings): clear stale pending and unsaved indicators after save Settings sections held their saved baseline in a mutable ref and computed the dirty count with useMemo keyed on the live values, so updating the ref on a successful save never re-ran the calculation. The masthead pending count and the sidebar unsaved dot stayed stale until the section remounted, making operators think the save had failed. Move the baseline into state behind a shared useSettingsDirty hook with separate load (reset) and save-success (markSaved) operations. markSaved adopts the submitted snapshot as the baseline only, so an edit made while a save is in flight survives and a failed save stays dirty and retryable. Migrate the five sections that used the pattern. * test(settings): await the save-failure retry assertion to avoid a race In the failed-save reconcile test, wait for the Save button to re-enable after the PATCH settles instead of asserting synchronously, so the retry check cannot race the isSaving reset. |
||
|
|
3c116466d9 |
refactor: drop the advisory policy-packs section and the findings cursor tooltip (#1369)
* refactor: drop the advisory policy-packs section and the findings cursor tooltip Two Security-page cleanups from review. - Remove the advisory policy-packs catalog from the Policies tab. It was information-only and disconnected from the scan_policies enforcement engine, so it read as duplicated. The tab now hosts only the enforcement manager, which is paid, so the Policies tab is hidden for Community (with a deep-link guard) and the Overview's enforcement hint is gated to match. The backend policy-packs catalog and route are kept as a dormant foundation. Delete the orphaned PolicyPacksTab component, its test, and the unused frontend pack types. - Drop the cursor-follow tooltip from the findings severity badge (Secrets and Compose risks), matching the Images table. - Clarify that Compose risks is a Trivy security-misconfig audit, distinct from Compose Doctor's deploy-readiness preflight, in the tab copy and the docs. * chore: re-run CI |
||
|
|
4610a433e6 |
fix(fleet): scope Stop-by-label to stack labels with a typed suggestion source (#1368)
* fix(fleet): scope Stop-by-label to stack labels with a typed suggestion source The Fleet Actions "Stop by label" card labelled its target field generically as "Label", so a same-named node label could look like a valid stop target in a destructive workflow. The action has always matched stack labels only, but nothing in the copy or the data flow made that explicit. Add a stack-label-only suggestions endpoint and make the scope unmistakable: - New GET /api/fleet/labels/suggestions aggregates the per-node stack labels into a name-keyed list with stack and node counts (admin-only, central DB, covers every configured node including offline remotes). Node labels are never folded in. - The card now sources its autocomplete from that endpoint and renders each suggestion with its stack and node counts via a typed FleetStopLabelSuggestion model, so node-label data cannot be fed into this destructive card. - Copy is explicit throughout: "Stack label" target field with a helper line that node labels are not used, a clear "0 matching stacks" readout and a "No stacks are assigned to this stack label" empty preview, and confirm and result copy that references stacks and the stack label. - Docs updated (fleet-actions, stack-labels) and tests added on both sides, including node-only exclusion, name collision, multi-node counts, the zero-stack preview, and the non-fatal suggestions-load path. * docs: correct stale Stop-by-label button and helper references The Stop-by-label walkthrough referenced a "Stop matching stacks" button and a warning callout that no longer exist on the card. Align the docs with the live card: the primary action is "Stop fleet", and the scope is stated by the helper line under the input. |
||
|
|
ef5a3f00a7 |
feat: add an on-demand node-wide security scan with live progress (#1367)
Add a "Scan this node" action on the Security overview that scans, in one pass, any combination of three types: image vulnerabilities, image secrets, and compose misconfigurations. Progress streams live into the deploy-feedback modal. - TrivyService.scanNode runs the selected scanners across the node's images and, for misconfig, every stack's compose file, behind a per-node lock and tolerant of per-item failures. The existing scanAllNodeImages becomes a thin vuln-only wrapper over the shared image loop, so scheduled scans are unchanged. - POST /api/security/scan-node (admin, scanner-gated) streams sanitized progress to the deploy terminal and returns a combined summary. Secret scans stream counts only, never matched values. - Frontend adds a "scan" action verb and a ScanNodeLauncher wired into the overview; the scan stays bound to the node it started on even if the active node changes mid-run. |
||
|
|
ebf66fd92a |
feat(settings): add Stacks section for stack workflow preferences (#1366)
Move the browser-local Deploy progress, Progress style, and Diff preview before save controls out of Appearance into a new Stacks section under the Infrastructure group. These are stack lifecycle and editor workflow preferences, not visual style, so Settings now groups them where operators expect to find them. Add a browser-local masthead scope so these localStorage-backed sections read SCOPE browser instead of the misleading global, and apply it to both Appearance and Stacks. Control behavior, storage keys, and backing hooks are unchanged; this is an information-architecture move only. |
||
|
|
6cbab661a0 |
feat: match the Security masthead to the primary-page hero and make History search realtime (#1365)
- Masthead: add `size` and `subtitle` to PageMasthead and render the Security masthead at the hero title size with a one-line posture summary, so its height and weight match the Home and Fleet mastheads. The compact size with no subtitle stays the default, so Settings, Console, and Logs are unchanged. - History search now applies as you type (a short debounce, no Enter press), matching the Fleet overview search. It stays server-side so the query searches every completed scan, not just the loaded page. |
||
|
|
3d39d856a3 |
feat: chart-led Security overview with sortable Images and History tables (#1364)
* feat: chart-led Security overview with sortable Images and History tables Refine the Security page around the existing design system and add the data the dashboard needs. - Overview leads with four charts (30-day risk trend, severity donut, top exposed images, findings by type); the signal-rail counts become a secondary summary, and the scanner and deploy-enforcement posture follow. - Images becomes a recessed table with search, a severity filter, sortable columns, a last-scan column, and inline scan actions; the findings cell is clickable into the scan sheet, and the per-row cursor tooltip is dropped where the columns already carry that information. - Policies puts deploy-enforcement first, collapses the policy packs into an accordion, and uses the standard primary button for Add policy. - Suppressions and acknowledgements move their titles and Add buttons outside the cards, matching the Fleet tab layout. - History switches from the detail sheet to an inline table (search, sortable columns, two-scan compare, pagination); the now-unreachable scan-history overlay is removed. - Add GET /api/security/overview/trend, a node-scoped daily critical/high rollup backing the risk-trend chart. - Extract the shared image-scan hook and the severity classifier, and harden the overview data fetch so a malformed non-critical response can never read as a clean security state. * fix: treat malformed Security responses as errors, not empty or clean states Address an independent review of the data-fetch paths so a 200 with an unexpected shape can never read as a benign "no findings" view. - SecurityView: validate that the image-summaries body is a scan-summary map; an unexpected shape now sets the error state instead of an empty map. Isolate the trend fetch in its own self-catching promise so a transport failure on the non-critical chart can no longer poison the overview or summaries error state. - useImageScan: only a "completed" poll counts as success (a malformed or unknown status now throws), and a failed post-scan summaries refresh is logged instead of silently dropped. - HistoryTab: a 200 whose body lacks an items array is treated as an error, not an empty "no completed scans" list. |
||
|
|
1b96f3b980 |
feat: add a compact icon-only top navigation toggle (#1363)
* feat: add a compact icon-only top navigation toggle Add a browser-local "Top navigation labels" preference under Settings > Appearance. With it off, the desktop top navigation renders icon-only; each destination keeps an aria-label, gains a hover/focus tooltip, and stays reachable from the command palette. The setting defaults on, so current behavior is preserved, and the mobile navigation always keeps its labels. Also left-align the desktop nav (previously centered) and shorten the longest nav label from "Auto-Update" to "Update" so the bar scans faster. * feat: let the icon-only top nav be left or centered Add a "Top navigation alignment" preference under Settings > Appearance that appears only when top navigation labels are off. It places the icon-only bar against the left edge (the default) or centered. With labels on, the nav always stays left so the longer labels read from the edge. The choice is browser-local and persists per device. |
||
|
|
2a4955f56d |
feat: add dedicated Security page and policy-pack foundation (#1362)
* feat: add dedicated Security page and policy-pack foundation Bring vulnerability scanning, scan history, suppressions, Compose risks, secrets, policy packs, and scanner setup into one node-scoped Security command center instead of scattering them across Resources and Settings. - New top-level Security view with Overview, Images, Compose risks, Secrets, Policies, Suppressions, History, and Scanner setup tabs (status masthead + signal rail; controlled tabs with deep-link support). - Backend: GET /security/overview rollup and GET /security/policy-packs static catalog (auth-only, Community). DatabaseService gains an uncapped scan-status count and a node-eligible block-policy count, and getImageScanSummaries now projects secret and misconfig counts. - Reuse existing surfaces: the scan-history sheet, the control-governed suppression and acknowledgement panels, and the scan-detail sheet (now with an initial-tab prop so it opens on the matching finding type). - Extract a shared SeverityBadge (from Resources) and a TrivyManager (from Settings) so both surfaces render identical controls. - Resources "Scan history" now links into the Security page History tab. - Docs for the new Security surface and tests for the new endpoints, helpers, nav wiring, and tabs. * refactor: consolidate scanner and policy management onto the Security page Remove the Settings "Vulnerability Scanning" section now that the Security page covers the same ground, with every option preserved: - Scanner install / update / uninstall / auto-update live on the Scanner setup tab (TrivyManager). - Scan policies, the honor-suppressions toggle, and the replica managed-by-control / demote controls move into a new ScanPolicyManager on the Policies tab (paid; Community sees only the policy-pack catalog). - CVE suppressions and acknowledgements remain on the Suppressions tab. Wiring removed: the registry section and the now-empty Security settings group, the SectionId, the SettingsSectionContent case and the isPaid prop it was the sole consumer of, and SecuritySection itself. The dashboard configuration-status "Vulnerability scanning" row now navigates to the Security page Policies tab. Docs that pointed at "Settings -> Security -> Vulnerability Scanning" are swept to the relevant Security page tabs. * fix: harden Security page scanner refresh, policy-load errors, and secret-only badges Address independent-review findings on the Security page: - Scanner setup now refreshes Trivy state when the active node changes, so the displayed scanner status matches the node TrivyManager's actions target (both follow x-node-id). Previously, switching nodes on the tab left stale state. - ScanPolicyManager surfaces an explicit error state on a failed policy fetch instead of falling through to a false "No scan policies configured". - The shared SeverityBadge and the Images findings column no longer label a scan "clean" when it has secrets or misconfigurations but no CVE severity (highest_severity is derived from vulnerabilities only); they show a "Findings" state and the secret/misconfig counts instead. - The Overview enforcement note points to the Policies tab, not the removed Settings section. - The History tab auto-opens the scan-history sheet only on a deep-link (mount with the History tab active), not on every manual tab selection. Adds tests for the badge secret/misconfig state and the policy-load error state. |
||
|
|
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. |