mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-10-25 21:20:25 +00:00
docs/tutorials-batch-3
944 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9f480e133b |
chore(whats-new): replace the feed with the 0.98.0 entries (#2170)
A new release now opens on a fresh What's New list instead of appending to the previous one. The feed holds the 13 entries that shipped in 0.98, with screenshots for the seven most visible changes, and drops the 0.97 entries, the RBAC entry from 0.97.0, and three smaller 0.98 items that their feature pages already cover. Entry ids are prefixed with the release (v0-98-...), and an authoring check fails when ids from more than one release share the file. That keeps a leftover entry from shipping and guarantees the new last id differs from any id a user has already seen, so everyone gets the unseen indicator once while fresh installs stay quiet. The eight screenshots used by removed entries are deleted. The new ones are element crops with host names, node names and repository paths replaced. |
||
|
|
5b41c0512f |
docs: reorganize GitOps documentation into one section (#2167)
* docs: reorganize GitOps documentation into one section Add a GitOps overview that defines the model once (Direct sources versus Blueprints, the chain from commit to container, the three authority steps, the policies, source states, executable identity). Split the oversized Git Sources and Blueprints pages into focused pages for Git authentication, provider and CI webhooks, repository secrets, and Git-managed Blueprints, and trim the remaining pages to their own slice. Group the GitOps pages in the navigation and split the old "Deploy and automate" group into "Deploy and recover" and "Automate". Existing page slugs are unchanged. Correct the provider hook labels to match the app, and repoint What's New links to the new Git-managed Blueprints page. * docs: add current GitOps screenshots and correct UI labels Retake the Git Sources, Blueprints, and GitOps portfolio screenshots on the integration image, and add screenshots to the new GitOps pages (answer, path, and evidence block; SSH deploy key form; provider hooks; Secrets tab; Convert to Git, placement review, and rollout controls). Correct labels to match the app: Deploy key (SSH), the provider hook event scopes, and the encrypted source policy buttons. The change plan dialog shows the stack name, not the commit. * docs: map the GitOps policy names once in the overview Evidence, the policy controls, and the Git source form name the same settings differently. State the mapping in one place. |
||
|
|
77026fc84f |
fix(deps): bump dompurify to 3.4.17 (#2168)
Two advisories (GHSA-cg2g-xh64-v4q7, critical, and GHSA-mrv2-3pqr-j5jr, high) affect dompurify up to 3.4.16 and fail the dependency audit gate. dompurify reaches the frontend only through monaco-editor, which pins 3.4.15, so raise the existing override to ^3.4.17 and refresh the lockfile. |
||
|
|
935158b5e6 |
feat(resources): bring the Resources page to the design system (#2156)
* fix(resources): load the Resources page and volume prune without the slow image df On a host that uses the containerd image store, the image section of `docker system df` takes about 30 seconds, and the Resources page waited on it for its footprint figures. Pruning volumes repeated that full df once per volume, and the prune preview ran into its 12 second timeout. - The disk-usage figures now come from calls that answer in milliseconds: a shared-size image listing plus the volume and container sections of df. The reclaimable image figure is the bytes unused images do not share with another image; the exact df is used only when the daemon does not report shared sizes. - External and managed volume bytes were always zero because the volume listing carries no usage data. They now come from the volume section. - Prune planning and execution read only the volume section for volume usage, ask for container sizes only when containers are planned, take shared image sizes from the listing, and inspect idle networks in parallel. When volume usage cannot be read the plan fails with an error instead of reading as nothing to prune, and execution skips the volume with a clear reason. - The Resources page loads the tables and the disk usage separately, so the tables no longer wait on the figures. Refreshing after an action keeps the rows on screen, and a failed load shows an error with Retry instead of an empty table. On a node with 200 volumes and 85 images, the disk-usage request went from about 31 seconds to 0.2 seconds, prune previews from a 12 second timeout to under 0.4 seconds, and pruning 200 volumes now takes about 1.5 seconds. * feat(resources): bring the Resources page to the design system The Resources page had a reclaim banner behind an opt-in setting, a disk footprint card, four equal-weight Quick Clean tiles, and per-node chrome of its own. It now follows the same shape as the other primary pages. - A hero masthead gives the one answer: how much a cleanup would free, Tidy when nothing is left, Measuring disk while usage loads, and Disk usage unknown when the node could not report it (never Tidy). Managed and external bytes sit beside it. - Reclaim and a cleanup menu (prune unused networks, prune stopped containers) sit in the tab strip. Images and Volumes each have their own Prune unused. Every prune opens an itemized preview with a scope switch (Sencho-managed by default, or All Docker) that rebuilds the plan when it changes. - The reclaim banner and its opt-in setting are removed, along with the footprint card and the Quick Clean tiles. - Filters use the shared segmented control, scans use the shared scan flow, and confirm buttons show progress through the shared busy label. - The Unmanaged tab is named External. Its rows use checkboxes and its project headers are quiet rows. A Rollback protected badge on a stack-held image opens the Rollback tab. - The docs page, the pages that mention the old tiles, and the screenshots are updated. |
||
|
|
880e6c6489 |
fix(resources): load the Resources page and volume prune without the slow image df (#2155)
On a host that uses the containerd image store, the image section of `docker system df` takes about 30 seconds, and the Resources page waited on it for its footprint figures. Pruning volumes repeated that full df once per volume, and the prune preview ran into its 12 second timeout. - The disk-usage figures now come from calls that answer in milliseconds: a shared-size image listing plus the volume and container sections of df. The reclaimable image figure is the bytes unused images do not share with another image; the exact df is used only when the daemon does not report shared sizes. - External and managed volume bytes were always zero because the volume listing carries no usage data. They now come from the volume section. - Prune planning and execution read only the volume section for volume usage, ask for container sizes only when containers are planned, take shared image sizes from the listing, and inspect idle networks in parallel. When volume usage cannot be read the plan fails with an error instead of reading as nothing to prune, and execution skips the volume with a clear reason. - The Resources page loads the tables and the disk usage separately, so the tables no longer wait on the figures. Refreshing after an action keeps the rows on screen, and a failed load shows an error with Retry instead of an empty table. On a node with 200 volumes and 85 images, the disk-usage request went from about 31 seconds to 0.2 seconds, prune previews from a 12 second timeout to under 0.4 seconds, and pruning 200 volumes now takes about 1.5 seconds. |
||
|
|
48b8d698b3 |
feat(gitops): observe remote rollout targets over the node proxy (#2149)
* feat(gitops): observe remote rollout targets over the node proxy A health rollout policy could not gate a rollout target on another node. The gate runs on the instance that owns the rollout, a remote node's Docker daemon is not reachable from there, so every observation of a remote target ended unknown and a policy other than observe was refused before dispatch. The node that hosts the stack now serves its own runtime facts at GET /api/stacks/:stackName/health-observation, with the declared Compose restart intent when asked for it, and the gate reads them over the node proxy. Local nodes keep the in-process Docker read, and the gate keeps one observation engine, one run row and one attribution path. health-gate-observation-v1 is advertised by every build that serves the route, and the placeholder gitops-health-rollout-policy is never advertised, so a hub that predates the protocol still refuses a remote target before dispatch. Failures stay fail-closed: a node that does not report the facts a verdict needs ends the observation unknown at the first poll, any other unreadable poll keeps the three-strike budget, and a clean exit whose declared intent could not be read ends unknown rather than failed, so a rollback policy cannot restore a workload that may have completed exactly as declared. A node row this instance does not have is unreadable rather than local. The node-proxy JSON read is extracted into one helper, and the leaf fetches in the artifact-resolve path use it. * chore(security): make the compose path containment checks analyzer-visible The CodeQL javascript-typescript check reports js/path-injection at two sinks that a rollout health observation reaches with a request's stack name: FileSystemService.hasComposeFile (the fsPromises.access call) and ComposeService.renderConfig (the spawn cwd). Both are guarded already, but not in a form the analyzer credits: the first called the wrapped assertWithinBase helper, and the second relied on a check scoped to the branch that assigned the working directory rather than one that dominates the sink. Both now carry the inline barrier the codebase already uses elsewhere for this rule, in the same scope as the sink. No behavior change. assertWithinBase and the inline check use the same rule, and every caller of hasComposeFile already passes a stack directory under the compose base. renderConfig re-checks the same root its branch check validated, immediately above the spawn. * chore(security): keep the hasComposeFile guard in the shape the analyzer reads The first pass combined the prefix check with an equality disjunct, which the path-injection query does not model as a guard. The single startsWith check is the shape the codebase already uses at its other sinks, and it is what clears the alert. The compose base itself is not a stack, so refusing it is the contract every caller already relies on; the test pins that too. * fix(health-gate): decide definite failures before an ambiguous clean exit An exit with code 0 whose declared Compose restart intent cannot be read is undecidable on its own, but it was ending the observation immediately. A peer that was unhealthy or had exited non-zero later in the same poll was never inspected, so the run ended unknown and a retry, stop, or rollback policy was replaced with a pause. Hold the ambiguity until the rest of the poll is inspected, in both the stack and the service scope, and decide it only where it is the sole unsatisfied evidence. At the window end definite failures are judged first: a missing expected container or replica, an unhealthy container, and a replica count the gate could not satisfy all outrank an ambiguous peer. A service replica count can now no longer be satisfied by a replica the gate never watched while a tracked replica exited unexplained, and an unhealthy replica of the operated service that appears after the gate armed fails on the poll where it is seen, since a later recovery would erase the evidence. Scale-0 services keep their previous accounting: a failed leftover is ignored, while a running container or a declared clean one-shot is still count drift. Adds regression coverage for both container orders, both scopes, unhealthy and non-zero-exited peers, a missing replica at the window end, untracked replacements, the unhealthy-recovery case, and the scale-0 boundary. |
||
|
|
1a52b5052e |
fix: group the GitOps portfolio filters into one popover (#2154)
The portfolio filter row was five flat dropdowns that wrapped across the toolbar. It is now one Filters popover with labelled sections (Attention, Target, Node, Drift, Evidence) and a count badge, the same shape as the Fleet toolbar. Stack and Blueprint scopes set by other surfaces still show as removable chips, and Clear filters still resets everything, including filters only a deep link can set. Option groups expose their section label and pressed state to assistive technology, and the Node and Drift dropdowns are tied to their labels. The Fleet filters popover now lays out its Critical Only button in the same row as the other sections. |
||
|
|
d8891f3b5e |
fix: keep outline buttons and filter selects flush with the card in the Dim theme (#2153)
Outline buttons and four filter selects (Fleet toolbar, GitOps portfolio, Readiness findings, Security images) painted with the page background, which in Dim is darker than a card, so they read as black holes on cards. They now use a control fill that is card-level in Dim. OLED and Light keep the page background exactly as before. |
||
|
|
495bac574a |
fix: polish notification actions, topology icon, minimized progress, security sheet and timeline lanes (#2151)
- Notification rows reserve room for every visible action, so the mute menu no longer sits on top of long messages. - The Networking topology tab and the "Show in topology" row action use the graph-star icon instead of the Git branch icon. - A minimized deploy progress pill now closes itself once the task succeeds, even if the pointer was over the dialog when it was minimized. - The scan comparison sheet no longer refetches and blanks every time its parent re-renders. - The Security masthead drops the review action that the "Why Action needed" card already offers. - Timeline pills sit centered in their own lane instead of straddling the lane above, and compose file backups now appear in the Backups lane. |
||
|
|
e9f824266b |
feat: open a published port at its configured service URL (#2124)
* feat: open a published port at its configured service URL
Store the address as the compose label sencho.service.url so a port click follows that host, and keep the generated link when the label is absent.
* fix: reject an escaping compose path before reading it
The compose filename scan checks the path stays inside the compose directory before it stats the file, so a rejected path is not treated as a missing file.
* fix: keep inherited labels and unsaved edits when a service URL is saved
A URL edit copies inherited labels onto that service, a changed compose filename keeps the draft unless the operator reloads, and a missing URL list still opens the generated link.
* fix: keep a service URL edit on the service that was opened
A shared label or service anchor is copied before the edit, clearing a merged URL does not restore the inherited address, and a running container opens only its own saved URL.
* fix: keep empty labels and dynamic ports on a service URL
A URL edit dropped empty labels and failed when a nested anchor was still used. A Docker-assigned port now opens that service's own URL, and the pencil stays unavailable when the service is not in the open compose file.
* fix: keep a service URL on the service that was edited
A shared URL value no longer changes another service. Open App does not follow a different service's dynamic address, a named port on its own bind address stays clickable, and a repeated list label takes the new address.
* fix: keep a service URL dialog on the stack it was opened for
The dialog now records the node and stack it was opened on, closes when that selection changes, and refuses the save before it can write a different stack. Clearing or replacing a list alias updates only the edited service. A URL supplied by another compose file stays on that file, and reloading the compose source refreshes the cached addresses.
* fix: edit a service URL without rewriting the rest of the compose file
A save splices the label into the original text, so octal modes, line endings, and unrelated keys stay as written. A node with no host keeps the plain port, a refresh keeps the last URL, and Open App opens its tab before the lookup.
* fix: keep service URL edits and compose saves inside the edited stack
- Refuse a compose file, or a stack directory, that resolves outside
<compose root>/<stack>, so a read or save of one stack cannot reach a
sibling stack's compose file. Covers saveStackContent too.
- Edit flow-style labels and services in place, copy anchored flow
nodes without their defining key, keep source tokens such as 0440 and
022 when labels are copied, append after the last real label (before
trailing comments, and at an end of file with no newline), and keep
the separator when an empty URL value is set or cleared.
- Check every patch before it can be saved: the file is read with YAML
types and as untyped text, and anything that changes outside the URL
label, drops a comment inside brackets, or touches a file with a
custom YAML tag is refused with a clear message.
- Return the cached service URLs only for the node and stack they were
read for, so a port never links to the previous selection.
- Log stacks the networking summary skips instead of dropping them
silently.
* test: assert the networking summary skips a sibling-linked stack
A stack whose compose file resolves outside its own directory is skipped
with a warning. Assert the linked stack stays out of every bucket, the
healthy stack is still summarized, and the skip is announced.
* fix: keep service URL edits effective and prototype-safe
The effective-anatomy response now reports, per service, whether the open
main compose file controls the sencho.service.url label (serviceUrlEdit).
That is established from the authored chain: the label key presence is read
in the files merged after the open one (deploy spec order, or the resolved
main file plus the auto-discovered override). A later file that sets the key
blocks the edit whatever its value (invalid, interpolated, or equal); a later
file that uses include, extends, a custom YAML tag, or COMPOSE_FILE, or that
cannot be read, refuses the edit as unverifiable. The pencil lock, the save
guard, and a fresh re-read immediately before the write consume this
metadata, so the editor no longer infers ownership from the label value or
its absence. Post-save verification stays.
Service-keyed maps are built on null prototypes and read through own-property
lookups across the backend extractor, the response parser and resolvers, and
the local anatomy facts. Services named constructor, toString, hasOwnProperty,
or __proto__ now resolve the generated link when they have no label and keep
an own override when they do.
* fix: refuse a service URL edit the compose chain would shadow or reveal
A later bare `sencho.service.url` list item (`- sencho.service.url`) sets
the key to an empty value and shadows the open file, so the source
authority now counts it as a key contribution like the `key=value` form.
Clearing is not the mirror of setting: removing the open file's direct
label can surface an earlier chain file, the open file's own include, a
service's extends target, or its label_file. The anatomy response now
carries a per-service clear authority alongside the set authority, the
editor refuses an empty save the chain would keep winning or reveal, and
the patch helper refuses the same layouts before writing. Setting a URL
is unaffected.
The patch helper also treats a bare list item as the key when setting or
clearing, so a set no longer leaves a trailing empty entry that renders
as an empty value.
Operator docs describe the set and clear limits accurately.
* fix: refuse interpolated label keys and honor explicit dynamic ports
Compose interpolates a list label's key, so `- "${URL_LABEL}=https://app"`
sets `sencho.service.url` even though the authored source never names it.
A later file using that shape reported the open file as editable and the
write did not take effect; an earlier or later one failed to block a clear.
Such a key now marks the service unverifiable, so both guards refuse before
the save. A literal key with an interpolated value stays a known set.
`published: 0` (from `"0:80"` or `{ target: 80, published: 0 }`) asks
Docker to assign the host port, but the dynamic-owner predicate treated it
as a fixed host, so a container-port click and Open App fell back to the
generated node address. It now counts as automatic allocation, leaving UDP
binding, named-service isolation, and ambiguity refusal unchanged.
* fix: keep a saved service URL effective and a port link on its owner
- Read the open compose file's own source in the service URL set
authority: a labels list whose key is interpolated (`- "${NAME}=..."`)
makes that service unverifiable instead of editable, including when the
file's other facts cannot be traced (a top-level `include` or a custom
tag) and when the file cannot be read at all.
- Refuse a set and a clear in the editor buffer when the target service's
label list carries an interpolated key, so an unsaved draft is caught
before any write.
- Open a named container's own automatic binding URL on its card when
Docker assigned it the host port, even when another service's saved
file declares the same number, and refuse a saved-only single fixed
declaration when another service's automatic binding could hold it.
- Carry the runtime main-port owner (`com.docker.compose.service`) from
the bulk stack status through to Open App so it opens the address of
the running service that owns the port.
* fix: keep a runtime container card on its own service URL
- Treat a named runtime container as direct ownership evidence: when a
card carries both the compose service and the running container's own
port, that service's configured URL applies even after the saved port
declarations changed or were removed without a redeploy.
- Keep saved-model inference and its ambiguity refusals for callers
without runtime port data, such as the anatomy footer.
- Cover the changed-port, removed-binding, hostless-remote, and rendered
anchor cases in resolver and component tests.
- State in the stack management docs that the configured label applies
first and the generated address is the fallback.
* fix: keep merged label text and a runtime card's generated link
Clearing a service URL from a labels map that also merges a YAML anchor
materialized the whole map from parsed values, which deleted operator
comments inside the map and respelled unrelated scalar tokens. The clear
now copies inherited entries from their own source text, leaves direct
entries untouched, and refuses layouts whose text cannot be kept.
A running container whose compose service has no URL override fell
through to saved-model inference and could turn ambiguous when other
saved services claimed the same host port. Runtime ownership now answers
with the service's own URL or the ordinary generated host and port.
Regression tests cover the preserved comments and tokens (block, flow,
CRLF, merge lists, nested merges, shared anchors) and the generated
anchor plus copy target for a runtime owner without an override.
* fix: wait for the first service URL lookup before opening a port
A first effective-anatomy lookup was read as "no override", so a
published-port link and Open App could open the generated host and port
while a configured service URL was still being fetched. The in-flight
entry now stays pending: the port renders as plain text until the outcome
is known, and Open App opens the tab in the click and navigates once the
lookup resolves. Completed outcomes (no label, a failed load, an older
node) keep the generated fallback, and a refresh that still holds the
prior model keeps rendering it.
* fix: keep a long service URL on the card and a refused path honest
A long host now truncates so the copy button and pencil stay clickable, and every port link keeps its open arrow. Clearing the only service URL removes the labels key, including when the address has a fragment. Opening a stack the server refuses shows that reason. Effective anatomy refuses a compose file or discovered override that links outside the stack. The editor shows a new URL only after the save is accepted.
* fix: keep the port link visible and drop a sole URL list
The port mapping stays on the card when the host is long, and on a phone the identity line wraps above the action buttons. Clearing a labels list that only holds the service URL removes the labels key, while a comment still keeps an empty value.
* fix: keep a cleared service URL renderable and the port controls reachable
A clear that left the labels map with no entries wrote `labels:` with no
value, which Compose rejects ("must be a mapping"), so the stack stopped
rendering and every pencil in it disappeared. The key now goes with its
last label, and a comment the removal would drop keeps the entries as an
empty map or list under the key instead. That covers a clear through a `<<`
merge, a sole label with a trailing comment, a comment line closing the
block, and a shared anchor whose other consumer keeps its address. A comment
between the removed entries is still refused before any write.
The container list now clamps to its pane, so a long host truncates and the
port link's copy button and pencil stay reachable at 1440px and below
instead of sitting past the pane edge. The copy button also gets the 44px
phone target the pencil already had.
A refused compose path answers with the condition instead of "path is not
allowed", and the parser no longer warns about the custom tags those edits
refuse.
Every layout the clear can produce renders under `docker compose config`.
* fix: keep the port line readable in a narrow container pane
The pane clamp left the action icons at full size, so the name and port link collapsed. The card now wraps those icons once the name column would drop below 12rem. A refused URL edit stays in the dialog until the address or the compose buffer changes.
* fix: keep the service URL editable after a failed list load
A failed first load keeps the generated link, explains that the list is unavailable, and offers Retry. A later failed refresh keeps the last address and the pencil. The pencil tooltip matches the copy control.
|
||
|
|
7df1514ff1 |
chore(deps): bump the all-npm-frontend group across 1 directory with 25 updates (#2146)
Bumps the all-npm-frontend group with 25 updates in the /frontend directory: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.23` | `1.2.1` | | [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.11` | `1.4.1` | | [@radix-ui/react-context-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/context-menu) | `2.3.7` | `2.4.1` | | [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.23` | `1.3.1` | | [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.24` | `2.2.1` | | [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/hover-card) | `1.1.23` | `1.2.1` | | [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.15` | `2.1.17` | | [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.23` | `1.3.1` | | [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.18` | `1.4.0` | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.7` | `2.4.1` | | [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.15` | `1.2.0` | | [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.7` | `1.6.1` | | [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.3` | `1.5.0` | | [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.21` | `1.2.1` | | [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.16` | `1.4.1` | | [cronstrue](https://github.com/bradymholt/cRonstrue) | `3.27.0` | `3.32.0` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.52.0` | `1.57.0` | | [motion](https://github.com/motiondivision/motion) | `14.0.0` | `14.2.0` | | [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.6.7` | `1.8.1` | | [@testing-library/user-event](https://github.com/testing-library/user-event) | `14.6.7` | `14.6.8` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.6.4` | `26.6.5` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.1.1` | `6.1.2` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.71.0` | `8.71.2` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.3.2` | `8.3.4` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.3` | `5.0.4` | Updates `@radix-ui/react-alert-dialog` from 1.1.23 to 1.2.1 - [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.11 to 1.4.1 - [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.7 to 2.4.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.23 to 1.3.1 - [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.24 to 2.2.1 - [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.23 to 1.2.1 - [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.15 to 2.1.17 - [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.23 to 1.3.1 - [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.18 to 1.4.0 - [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.7 to 2.4.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.15 to 1.2.0 - [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.7 to 1.6.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.3.3 to 1.5.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.21 to 1.2.1 - [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.16 to 1.4.1 - [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 `cronstrue` from 3.27.0 to 3.32.0 - [Release notes](https://github.com/bradymholt/cRonstrue/releases) - [Changelog](https://github.com/bradymholt/cRonstrue/blob/main/CHANGELOG.md) - [Commits](https://github.com/bradymholt/cRonstrue/compare/v3.27.0...v3.32.0) Updates `lucide-react` from 1.52.0 to 1.57.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.57.0/packages/lucide-react) Updates `motion` from 14.0.0 to 14.2.0 - [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md) - [Commits](https://github.com/motiondivision/motion/compare/v14.0.0...v14.2.0) Updates `radix-ui` from 1.6.7 to 1.8.1 - [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 `@testing-library/user-event` from 14.6.7 to 14.6.8 - [Release notes](https://github.com/testing-library/user-event/releases) - [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md) - [Commits](https://github.com/testing-library/user-event/compare/v14.6.7...v14.6.8) Updates `@types/node` from 26.6.4 to 26.6.5 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@vitejs/plugin-react` from 6.1.1 to 6.1.2 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.2/packages/plugin-react) Updates `typescript-eslint` from 8.71.0 to 8.71.2 - [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.71.2/packages/typescript-eslint) Updates `vite` from 8.3.2 to 8.3.4 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.3.4/packages/vite) Updates `vitest` from 5.0.3 to 5.0.4 - [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/v5.0.4/packages/vitest) --- updated-dependencies: - dependency-name: "@radix-ui/react-alert-dialog" dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-checkbox" dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-context-menu" dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-dialog" dependency-version: 1.3.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-dropdown-menu" dependency-version: 2.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-hover-card" dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-label" dependency-version: 2.1.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-popover" dependency-version: 1.3.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-scroll-area" dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-select" dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-separator" dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-slider" dependency-version: 1.6.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-slot" dependency-version: 1.5.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.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@radix-ui/react-tooltip" dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: cronstrue dependency-version: 3.32.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: lucide-react dependency-version: 1.57.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: motion dependency-version: 14.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: radix-ui dependency-version: 1.8.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@testing-library/user-event" dependency-version: 14.6.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@types/node" dependency-version: 26.6.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@vitejs/plugin-react" dependency-version: 6.1.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: typescript-eslint dependency-version: 8.71.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: vite dependency-version: 8.3.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: vitest dependency-version: 5.0.4 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> |
||
|
|
903419814c |
fix: serve the UI typefaces from the instance (#2132)
* fix: serve the UI typefaces from the instance The page loaded its faces from a third-party host on every visit, including sign-in. Bundle those faces and stop allowing remote font and style sources. * fix: keep the full subset coverage of the served typefaces Bundling the faces left only the latin and latin-ext subsets declared, so Cyrillic, Cyrillic Extended, Greek, Greek Extended, Vietnamese and the symbols2 block rendered in a system face instead of the selected one. Declare every subset the installed packages publish for the faces the Appearance preference offers, with the range the package subsets each face to, and hold the declarations to those ranges and to representative characters per script in the font test. |
||
|
|
6da06ce76e |
fix: keep readiness finding verbs inside the pane (#2127)
The findings table was wider than the fleet pane, so the resolving verb sat past a horizontal scroll. Size the columns as a share of the pane until there is room for stack and domain. |
||
|
|
712c96ac6b |
fix(editor): keep the compose toolbar inside the pane at 1280 (#2125)
The save cluster was laid out past the viewport, so Save & Pull Images was unreachable without widening the window. |
||
|
|
7a6682ed3f |
fix: pull the next commit on a manual Git-managed Blueprint (#2130)
* fix: pull the next commit on a manual Git-managed Blueprint A claimed manual source had no operator fetch: polling skips it, and stack Pull now stays refused. Pull source stages a candidate and leaves acceptance to Accept source, except an automatic policy, which accepts the same way a poll does. * test: supply a node list to the GitOps sheet mocks The authority row reads the default node from the node context. These mocks omitted nodes, so the sheet threw before it rendered. * test: type the Git source permission mock without unused parameters The default can implementation always returns false. Naming the permission arguments made lint fail. * fix: send the Blueprint id and report what Pull source did The claimed sheet posted the internal application id, so the pull never reached the Blueprint. An automatic pull could accept and start a rollout while the result only said the commit was fetched. * fix: report a continuing automatic pull instead of a failed one Acceptance can commit before preparation and rollout finish. Answer once that wait reaches its deadline, keep the source locked until the work finishes, and do not describe the deadline as a failed pull. * fix: report why an automatic pull cannot prepare a commit A completed preparation reason was replaced by the generic unresolved-artifact skip. Keep that reason, and describe the claimed sheet by the source actions it still offers. * fix: refuse a pull that would replace an interrupted apply's candidate Apply recovery stays eligible only while the interrupted generation is still the current candidate. Pull source waits in that case. An interrupted fetch still pulls, and a pull stays available once the candidate has moved on. * refactor: share the bounded follow-up after a Git-managed acceptance Accept source and an operator pull each had their own copy of the deadline, the continuation notes, and the slot hold. One follow-up now answers both. * fix: declare the post-accept deadline timer as const The deadline id is assigned once. A later assignment made the lint fail. * fix: confirm Accept source before it can start a rollout Accept source recorded the fetched commit immediately, including when stored rollout authorization can deploy it, and those actions were not on the sheet Overview. The confirmation names the commit, the approved targets, and whether a rollout can start, and confirm posts the generation that was on screen. * fix: read placement approval only on a Blueprint projection The accept preview read approvals on every projection, and the one with nothing to show has no approvals. The Blueprint check comes first so the build can read the placement ref. * fix: name the approved rollout scope and report settled failures Two confirmations could describe a destination the action would not use, and one result reported work as starting when it had already failed. Accept source named every observed target row as approved placement. Those rows are where an application runs, not where a rollout goes, and they disagree whenever placement moved since the last rollout ran: an approved node has no row until the rollout that places one, and a withdrawn node keeps one. A projection now carries the scope the placement approval froze, read from the rollout candidate and accepted only once the approval resolves for that set, which is the evidence the dispatch boundary already uses. Both confirmations read it through one reader and name the approved nodes, or say the scope cannot be read rather than naming a set they did not. The bounded follow-up used its continuing-work answer for a deadline and for a rejection alike, so a dispatch that had already failed was reported as starting, with nothing pending. The three outcomes are now distinct: only a deadline holds work still running, and a rejection reports the rollout as held with the failure named. * fix: bind a source confirmation to the request that carries it Pull source and Accept source asked the operator to confirm a destination and then sent nothing that named it. Fetch posted an empty body and acceptance posted only a generation, so execution read whatever the placement and the rollout policy happened to say at the moment it dispatched. An operator could confirm a placement for one node and deploy to another, and a dialog promising a waiting rollout could start one after another session armed the policy. Both bodies now carry what the dialog read: the placement approval the scope was resolved from, and whether the confirmation said a rollout could start. The description and the claim are frozen together when the overlay opens, so the words the operator agreed to and the state the server checks are one read of one projection. The claim is checked at every consequential boundary rather than only at the route, because each one is preceded by asynchronous work: at the route and at the controller entry, again before an automatic acceptance after the reconcile, and again in the handoff after preparation. A refusal that lands after the acceptance committed is held with the reason instead of answered, since the reconciler re-drives any accepted generation with nothing to check it against and a refusal that recorded nothing would be undone by the next tick. The hold carries an operator origin, so only a person clears it, and the recovery is the existing one: review the placement, then resume the rollout. The destination is compared exactly, because the dialog named specific nodes. The rollout promise is compared one way: a confirmation that allowed a rollout and finds the policy waiting for an operator does less than it said, and the dispatch gate already reports that, while the reverse would start something the operator was told would wait. A manual or review pull only fetches, so it asks for nothing, and the poll and resume paths send no confirmation at all because a cadence is configured rather than confirmed. * fix: keep the confirmation hold inside the mode that can clear it Three ways the refusal did not survive contact with the rest of the model. The hold was recorded below the rollout authorization policy gate, so a manual source paused the rollout over a confirmation the handoff could never have acted on. A manual policy never dispatches from here, and its Authorize is already the operator's own decision against the placement as it stands, so all that bought was an extra resume step before an authorize the pause also blocks. The gate is back first and the confirmation is checked under it, so only an automatic policy, the one that would have dispatched on its own, is held. The hold outlived the Blueprint. Every resume route resolves a Git-managed Blueprint and answers NOT_GIT_MANAGED for the row afterwards, so a pause carried through Retire or Detach read as a paused rollout nothing could clear. Both demotion paths now clear it, which is what the Blueprint demotion already does with every other column it invalidates. And the module claimed the check ran at every consequential boundary, which is only true of the boundaries an operator action reaches. A cadence carries no claim to check, so the poll and the reconciler's re-drive are not among them, and the hold is what keeps them from undoing a refusal. Each correction is covered by a test that fails without it: a manual-policy acceptance whose placement moves during preparation is not held, an operator hold survives a later acceptance on the policy's own behalf, and retiring a Blueprint releases a held rollout. * fix: make a confirmed acceptance keep its placement past the request A confirmation carried only in a request body stops existing when the request answers, and two paths outlive it while both still dispatch. Preparation can run past its deadline and hand the handoff to the reconciler, and a restart re-drives any accepted generation with no request in the picture. The guard therefore protected the fast path and nothing else, which is worse than none: the operator was told to review a change that then deployed on its own. The acceptance now records the precondition on its own approval row, in the column that already existed there and was unused for this kind. A null recording is a real answer rather than missing evidence: an acceptance made by the poll records none, because a cadence is configured rather than confirmed. It is enforced in ensureRolloutAuthorization, the single funnel every mint passes and the only place an unattended dispatch reaches, so it covers the reconciler's re-drive and the startup preflight backfill as well as the request that carried the claim. Only a policy mint is bound: an operator mint is the decision the confirmation deferred, taken against the placement as it stands. A stale precondition holds rather than merely refuses, because a refusal with nothing behind it is re-attempted every reconcile and leaves a rollout queued with no reason attached. The acceptance itself stands, so the content is recorded and only its destination waits for a person. An application-scoped resume releases the recording; a target-scoped one does not. The same function now re-resolves ingredients after the registry preflight await and refuses when the placement approval or the frozen node set moved. The mint re-reads ingredients inside its write transaction, so a placement re-approved mid-preflight produced a rollout naming the replacement nodes with readiness evidence that described the old ones. That gap predates this work, but a request-local guard cannot see past an await, and a rollout generation is immutable once written. * fix: compare the whole ingredient record, and let an operator mint decide Two gaps in the durable guard, both found by reading it against the paths that reach it. The post-await comparison watched the placement approval and the frozen node set and nothing else. A source acceptance landing during the same preflight window changes the generation, the artifact set and the acceptance without moving either of those, so the check passed and the mint named the new generation with readiness evidence evaluated for the old one, which is the same defect the comparison was added to close. Every field the mint copies onto an immutable rollout generation is compared now. An operator mint is exempt from the guard, because asking to authorize the rollout is the decision the confirmation deferred. Nothing moved the recording on that path, so once the authorization lapsed the automatic policy held a rollout asking the operator to resume a placement they had deliberately authorized. An operator mint now re-points the recording at what was decided, inside the mint's own transaction, which also keeps the guard working for the next replacement. * fix: keep the whole source confirmation after the request ends A stored placement pointer let a later re-drive start a rollout the operator was told would wait, or deploy to a destination the confirmation never named. The acceptance now stores both facts, and unattended authorization reads that record rather than the request that already finished. * fix: show source confirmations and start a ready automatic resume Operators were asked to confirm a sentence they could not read, and Resume reported that an automatic policy would start a rollout it never dispatched. |
||
|
|
d48daa08ae |
feat: raise the Compact launcher quick-link cap to 10 (#2120)
Operators can pin ten destinations on the bar. The recommended starting set stays at six, and the server still accepts up to sixteen so a ten-pin list keeps syncing. |
||
|
|
7c99a65085 |
feat(settings): change fresh-install defaults for charts, log chips, progress style and stack guardrails (#2119)
* feat(settings): change fresh-install defaults for charts, log chips, progress style and stack guardrails Fresh installs now start with the Signature chart palette, per-service log chip colors, inline deploy progress, one retained rollback generation per stack, and the missing-required-env deploy guard on. Existing stored values are unchanged. * fix(settings): make appearance reset match the fresh-install chart default Reset to default was still applying the Calm preset, which put the muted chart palette back. It now restores upright headings, reduced effects, and the Signature chart palette, and the appearance screenshots match that look. |
||
|
|
23f87354c4 |
feat(stacks): rescan from the scan banner; recheck Blueprint name conflicts (#2118)
The post-deploy scan banner in the stack editor gains a Rescan verb. It starts the same scan on the node that owns the stack, follows it to the new result, and says so when the rescan finishes clean, since the banner then goes away. A second click while a scan is running starts nothing, and the scan-status read now reports whether one is running. Dismiss stays separate and still lifts when a new attempt lands. A Blueprint deployment stuck in Name conflict now says what to do and offers Open conflicting stack and Check again. Check again only re-evaluates the conflict: when the name is free the row returns to Pending and the next reconcile deploys it, with the usual state review for stateful Blueprints. It never deploys by itself, Git-managed Blueprints are left to their rollout, and an ownership check that cannot tell keeps the row in conflict. The two banner dismiss hooks are merged into one that keeps the existing storage keys and events. |
||
|
|
63220a7d92 |
feat(notifications): mark alerts read and mute them where they appear (#2117)
* feat(notifications): mark alerts read and mute them where they appear Add POST /api/notifications/:id/read, a node-scoped single-row read that answers 404 for an id that is not on that node. Recent alerts and the bell now carry a per-row Mark read, and Recent alerts gains the same admin-only Mute menu the bell has. Both run in place: neither navigates, and the bell stays open. * fix(notifications): keep a confirmed read from being undone and mute on the hub Refetch the feed after a confirmed mark-read so an older in-flight response cannot restore the unread row. Send mute-rule creation to the hub regardless of the active node, so Mute from Home works while a remote node is selected. Cover the Home wiring, the shared mute menu, and both behaviors with tests. * docs(notifications): describe mark read and mute on Recent alerts and the bell |
||
|
|
87db3f121e |
feat(drift): say how to resolve drift and dismiss it (#2116)
* feat(drift): let a team dismiss a drift finding on the ledger Drift findings can now be dismissed until they change, for a set number of days, or permanently. The dismissal lives on the finding's own row in the drift ledger on the node that owns the stack, so the ledger's detected to resolved lifecycle retires it: once the finding clears its row is closed, and a recurrence is a new finding with no dismissal. Each live finding is published with a stable id, a fingerprint of its structured facts (kind, service, compose value, running value, never the message), a count, and a per-kind policy. The Git managed-path conflict is not dismissable. Dismissing and restoring need exact deploy authority on the stack, the dismiss route re-reads the stack and refuses a finding that changed or cleared since it was seen, and the drift status and finding count keep counting dismissed findings. Image mismatch findings also carry whether the running image was built after the one compose names, read from the images' build times. Anything that cannot be proven reads as unknown. * feat(drift): resolve and dismiss drift findings from the Drift tab Each drift finding now names its fix and carries the verb that runs it: redeploy to match compose (confirmed first, through the editor's own deploy handler), start a missing service, or open the stack editor or Git source where the change is a decision about compose rather than the runtime. When the running image is provably newer than the one compose names, the editor leads and the redeploy confirm says it moves the service back to an older image. Verbs the account cannot run are dropped instead of left to answer 403. Findings can be dismissed until they change, for 7 days, or permanently, and restored from the dismissed list. The status keeps its verdict and counts the dismissed findings. A report from an older remote node, which cannot record a dismissal, offers no Dismiss. * docs(drift): document resolving and dismissing drift findings Describe the verb each finding kind offers, the redeploy confirm, dismissing and restoring, and who can run what. Rework the drift tutorial around the redeploy button on the finding, with fresh screenshots of the finding, the overflow menu, the confirm, the resolved state, and the Activity timeline. * fix(drift): scope dismissals and late answers to the stack they belong to Dismissing a finding now writes only that finding's own ledger row instead of reconciling the whole stack from a snapshot that may be behind a concurrent check, so it can no longer resolve or insert sibling findings or stamp the check time. The Drift tab ignores a re-check, dismissal, or restore that finishes after the operator moved to another stack or node, and closes an open redeploy confirm on a switch. Open stack editor needs permission to edit the stack, so a deployer is not shown a button that does nothing. The dismissal hook takes either a hub surface or a node endpoint, a restore that finds nothing re-reads the list, a dismissal's mode and expiry are one type, and the drift dismiss route bounds the count like the other dismissal routes. Adds tests for the dismissal lifecycle, the race, the failure paths, and the handler wiring. * docs(drift): say which buttons need permission to edit or deploy |
||
|
|
184f2ceacb |
feat(security): act on posture findings from the overview (#2113)
* feat(security): give posture reasons a key and fingerprint, and a dismissal surface Each posture reason now carries a stable key, a fingerprint of its structured targets, and a policy for how a team may set it aside. Blockers cannot be dismissed; evidence that is missing or old can only be dismissed for a set time. A confirmed image update names the stack and service behind it so it can be applied from the overview. * feat(security): act on posture reasons from the overview and dismiss review items Each reason in the review queue carries the verb that resolves it: review and apply a confirmed image update per stack, set exposure intent per service, check update availability again (and refetch), rescan the node, open the scan on its secrets or misconfigurations tab, or go to Triage or History. The masthead action runs the same verb. Review and informational reasons can be dismissed until they change; blockers never can. * test(security): isolate the SecurityView refresh test from the new reason hooks * docs(security): describe in-place verbs and dismissing review items * fix(security): tighten posture actions from review Scan failures are reported and only a finished scan refreshes the overview, an overview read for another node never drives actions on this one, a service behind several findings is listed once, and the per-key severity is one exhaustive record. * fix(security): match verbs to route guards and keep tab shortcuts Rescan node follows the node scan route's global permission, secrets and Compose risks keep their tab shortcut instead of opening an arbitrary scan, a verb that runs in place keeps the tab shortcut in a menu, accounts that cannot read dismissals skip the read, and failed scans are dismissed for a set time only. * fix(security): keep refreshing the overview after a failed node scan * fix(security): keep the overview overflow menu after Dismiss The review queue built its shortcut menu inside the verb slot, so Dismiss landed after the overflow icon. Pass that shortcut through the shared finding-row menu, which already keeps the menu at the trailing edge. |
||
|
|
f935fa7b1d |
feat(networking): resolve and dismiss networking findings in place (#2106)
* feat(networking): give networking findings a structural key and dismissals Networking finding ids were hashes of the message, so rewording a message moved every id. They are now `kind|stack|service|network|subject`, and each finding carries a fingerprint of its severity and structured targets, a target count, and the dismissal policy of its kind. Adds the networking dismissal surface: a per-kind policy record, a key parser that derives node, stack and kind, and routes to dismiss, list and restore. Doctor-only findings stay with Compose Doctor: acknowledged ones are kept and marked instead of dropped, and left out of the counts. * feat(networking): resolve and dismiss networking findings in place Each networking finding now carries its own verb and Dismiss, in the Findings tab and in the Overview "Operator attention" block alike. Exposure intent is set in a popover over the row, a Doctor-only finding is acknowledged through a confirm that says it changes update readiness, copy actions move to an overflow menu, and Compose Doctor's remediation line is finally shown. Dismissed and Doctor-acknowledged findings sit under a quiet "N dismissed" row with Restore, and the posture word still counts them. * docs(networking): describe in-place verbs and dismissing findings * fix(networking): tighten dismissals and Doctor acknowledgement from review Setting an exposure intent now drops the networking memo so the finding is re-checked at once. Acknowledge in Doctor is offered only when every open occurrence names a service, because a service-less rule is acknowledged for the whole stack. Service-less Doctor findings (such as port conflicts) stay separate cards, finding keys are length-bounded, a failed dismissals read keeps the last list and says so, and a partial acknowledge or clear reports how far it got and re-reads the list. Adds tests for the dialog and the hook. * fix(ui): order finding row actions as verb, Dismiss, overflow Enforce outline primary and split Dismiss chrome, move networking overflow into a trailing slot, and drop the mobile-only dismiss kebab. * fix(ui): match finding row comments to the controls The verb class does not set outline, and the overflow slot only renders what the caller passes. * revert: restore the original finding row action layout The outline split and trailing overflow order read worse than the previous row. * Revert "fix(ui): order finding row actions as verb, Dismiss, overflow" This reverts commit 6948a084960ab00cb639b3e1c6cf6d5d1e50dfb5. * fix(ui): keep finding row actions in one order with the overflow menu last Every finding row now lays out its controls as the resolving verb, then Dismiss, then the overflow menu at the trailing edge, whichever of them a row lacks. Networking used to place its overflow menu between the verb and Dismiss, so the order changed from row to row and Dismiss shifted whenever a row had secondary verbs. FindingRowActions takes the secondary verbs as `more` and owns the menu. A surface that has an overflow menu keeps its slot reserved on rows with nothing to put in it, so Dismiss lines up across rows. Fleet Readiness has no overflow menu and is unchanged. Below the md breakpoint the secondary verbs and the Dismiss options fold into one trailing menu instead of two adjacent ones. A dismiss in flight locks only the Dismiss items there, as it does on desktop. |
||
|
|
2fedeafbf7 |
feat(readiness): resolve readiness findings in place (#2101)
* feat(readiness): resolve readiness findings in place Each Readiness finding now carries the verb that fixes it where it is listed, instead of a link to another page: - Test connection, Start stack or stopped services, Check again, Capture recovery point, Take fleet snapshot and Scan node run on one click against the finding's own node. - Review update, Install scanner and Re-anchor go through an overlay first. The update itself runs through the same request the editor uses, so the policy gate and the progress panel still apply. - A verb is offered only when the account passes the route's own check; otherwise the row keeps its page link. Findings from a peer that predates the new fields fall back to the page link too. Update and recovery findings now carry the signal behind their reason and whether an update is known, and both feed the finding fingerprint so a new image or a changed blocker brings a dismissed finding back. Capturing a recovery point drops the cached rollup so the finding it clears leaves the list at once. The stack update request and its error parsing move out of the editor hook into one shared module used by the editor, Auto-updates Apply and the new verb. * fix(readiness): report update failures and verb errors accurately - A stack update that never got an answer (dropped connection) now toasts the error instead of leaving only the deploy panel. - A failed update says when the stack was rolled back and shows the cause and next step; a recheck warning replaces the success line rather than stacking under it. - A failed connection test reports its own reason instead of "could not reach"; a node scan with failures warns instead of reading as clean. - The re-anchor confirm picks the paused sync row, ignores a late lookup for another finding, and says when the anchor could not be read. - A refused or failed update still re-reads the check. - Reason facts of the wrong type from a peer are ignored. - Add tests for the shared update request, its hook, the lifecycle helpers and the verb failure paths; correct stale comments. * refactor(readiness): flatten the verb confirm description * fix(readiness): label the partial-stack verb for what it does The start route starts every container of the stack, so both the stopped and the partly-down finding now offer "Start stack". |
||
|
|
11312d84d3 |
feat(readiness): dismiss readiness findings until they change (#2100)
Findings on Fleet > Readiness can now be set aside for the whole team. One click on Dismiss moves a finding out of the list with an Undo, and it comes back by itself if the finding gets worse or changes. A chevron offers a 7-day or permanent hold. Each row also says in one line what to do about it. Dismissed findings are never hidden: the status line counts them and a quiet "N dismissed" row lists who dismissed each one, when, and for how long, with Restore. Dismissing never changes the matrix cells or the fleet state. Dismissals are held on the hub. The finding id names the node and stack, and the server derives permissions from it (deploy on the stack, manage on the node, admin for Control). The server reads the finding itself and refuses a dismissal if it changed after the operator saw it. A dismissal is retired only when a complete live read shows the finding gone, so an outage never drops one. Findings that restate the Security posture cannot be dismissed here, and findings about evidence that could not be read can only be dismissed for a set time. |
||
|
|
ca90588e4e |
fix(gitops): bound GitOps history with a retention window (#2105)
* fix(gitops): bound GitOps history with a retention window `gitops_history` is insert-only and the reconcile tick appends to it continuously, so it grew for the life of an installation. A new `gitops_history_retention_days` setting (default 30, range 1 to 365) seeds with the other GitOps settings and prunes history from the monitor's cleanup pass. Drained outbox rows are pruned with the history they announced; an undrained row gets a grace period past the window because the boot repair can still drain it from its payload, and a row still undrained after the grace is counted, logged, and dropped so it cannot accumulate for ever. The window is instance-local, like the artifact retry interval beside it: the history being pruned is the one this instance holds, and the monitor that prunes it runs here. * fix(gitops): keep each live application's newest history row through the prune `cleanupOldGitOpsHistory` deleted `gitops_history` purely by age, but `latestTransitionByApplication` reads `MAX(created_at)` per application and feeds the "last activity" fact the portfolio and application sheet render. An application with no history inside the window, because polling is off, a source is suspended, a node is detached, or a Blueprint is idle, lost every row and its activity column silently went empty. The prune now exempts each live application's newest row, so the claim the JSDoc, the PR body and the settings helper text already make is true. Scoped to live applications rather than to every application_id: a tombstone in `gitops_applications` is permanent, so an unscoped exemption would retain one history row per stack ever retired and leave exactly the growth this setting exists to stop. A retired application's rows are now pruned in full. Also folds in three follow-ups from the audit: - The prune is chunked to GITOPS_PRUNE_CHUNK_ROWS rows per table per pass, so a first prune on a long-lived install clears its backlog over several janitor ticks instead of holding SQLite's single writer for the whole delete. The method reports historyRemaining and outboxRemaining so the debug log says whether a pass is still catching up. - The outbox OR-delete is two statements, so the undrained branch uses idx_gitops_settled_outbox_undrained instead of scanning for the drained one. - The retention tests assert exact counts and exact surviving row ids rather than toBeGreaterThanOrEqual over a shared database. New tests pin each guard: a quiet application's newest row survives every window, its older rows are pruned, a tombstoned application clears entirely, an unsettled attempt is still recoverable after a prune both inside the window and outside it, and a backlog clears in bounded passes with the remainder reported. Removing the newest-row exemption turns 10 of the 11 tests red; removing only the liveness clause turns the tombstone test red. Docs: the GitOps portfolio page describes the window and its setting, and now states that the newest entry per application is kept, which is what the code does. * test(gitops): drop the unused db handles from the recovery assertions * fix(gitops): keep a settled history row ahead of its started row A reconcile attempt stamps both rows with the same time, and the prune broke that tie on a random id, so it could delete the settlement and leave startup recovery to reopen the attempt. Recency now follows insertion order, in the exemption and in each chunk. Remaining counts run only while developer mode is on, drained outbox deletes use an age index, and the settings copy says the kept row is the newest one for each live application. * fix(gitops): log the undrained rows a history prune actually deletes A pass is capped, so the warning was reporting the whole backlog. The history-window tests now fail if the permission, the page save, the monitor window, or the getter fallback stops matching the code. * fix(gitops): prune a superseded unsettled attempt past the window An unsettled reservation survives past the retention window only while it is that live application's newest row. Creating applications keep that row; detached applications do not. * fix(gitops): skip the history prune when the retention setting cannot be read A failed settings read used to fall back to 30 days and delete rows the operator had chosen to keep. The getter now throws, and that pass deletes nothing. A missing or unusable stored value still uses 30. * fix(gitops): restore the history window when the retention spec fails The retention spec restored the seeded 30 days as its last statement, not in a finally, so a failed assertion left the shared instance on 14. The CI retry then read 14 and failed the fresh-install assertion, turning one failure into a persistent red. Everything after the first save now runs inside a try whose finally puts the window back, so the spec leaves the instance as it found it whether it passes or fails. * docs(settings): list the GitOps history window in the retention reference The Data Retention table listed every other retention window and not the GitOps one, so an operator checking retention windows did not find it. The portfolio page described the setting in a paragraph, which is not where retention windows are looked up. Adds the row with its default and range, and a line under the table saying the window is set per Sencho instance and where the control lives. * fix(gitops): report an unmeasured prune remainder as unknown The prune reported a remainder of 0 whenever developer mode was off, so the diagnostic log read "none left" for a table it had never counted. It now reports null, and the log line says the remainder was not measured. Three smaller corrections from the same review: - The diagnostics allowlist omitted the history window, so a support bundle could not explain a timeline that had already been pruned. - The GitOps registry entry described every control as belonging to this node, while the retry interval and the history window are instance local. The sentence now names no scope it does not have. * fix(gitops): correct what the history window is documented to govern The copy said the window governs notifications. It does not: the prune deletes history rows and their outbox rows, and the bell entries those outbox rows announce age out under the Notification log window instead. An operator setting 7 days would have expected their bell to follow. It also promised to keep the database small, which nothing delivers. The prune stops old rows from being written; it never reclaims the file. - The helper now names the record behind the GitOps timeline, points bell entries at the Notification log, and says the window stops history from growing. - The settings reference gains the GitOps section it never had, with the window under it rather than under a per-node table that ends in a Save button the control does not use. - The portfolio page stops claiming a shorter window never changes what an application is doing: a webhook redelivery reads this same record, which the same paragraph already said. - A fraction below one day now clamps to a day instead of flooring to zero, which as a cutoff is now and would delete everything not exempt. The only caller passes an integer, so this is a guard, not a reachable fix. * fix(settings): snap a typed number chip to its own step on commit The chip clamped to min and max but never rounded, so typing 1.5 wrote "1.5" and every integer setting rejected it at the route with a bare "Validation failed" toast. Nothing was saved, so no data was at risk, but the control accepted input it could not store. Rounding to the control's step fixes that and leaves a genuinely fractional chip alone: the Docker janitor asks for 0.5 GiB and keeps it. Clamping still runs after rounding, so a fraction below the floor cannot round down under it. The settings reference also listed GitOps last under Infrastructure while the sidebar renders it third. * docs(gitops): replace the unsourced window figure with what the code does The helper and both docs said a window under about 3 days can let a Git host retry a webhook. Nothing in the code picks three days, and the dedupe the sentence describes only applies to the direct webhook path. The text now states the condition instead of a number: a redelivery can run the work again once the record of its first delivery is pruned. A provider endpoint keeps its own delivery record, pruned to a fixed depth, so the window does not affect it, and the sentence says so. * docs(settings): say where a remote instance's window is set The reference explained that the window belongs to an instance rather than a node, but not how to change the window of a remote one, which is on that instance. |
||
|
|
0ce9754766 |
fix(gitops): make automatic placement approvals reach execution (#2103)
* fix(gitops): make automatic placement approvals reach execution Two defects on the bounded automatic placement path. A removal was never auto-approved: the decision read `gitops_targets.connectivity`, a column seeded to null that no producer ever wrote, so a withdrawal resolved to `unknown_connectivity` even though the target projection already derived reachability from the recorded observation. The decision now reads the projection's own `targetConnectivity`, requires the node registry to have the node online (the same gate an addition passes), and carries an observation that cannot be decoded as a limitation, so the refusal names malformed evidence instead of sending the operator to check a node that answered. An Inline Blueprint's policy placement approval was recorded and then never read: an operational edit clears the combined approval, and the reconciler returned at the legacy gate before consulting the policy's approval. The reconciler now executes a plan a current policy placement approval covers, under seven conditions: the application is inline, the placement policy is still the one that could have written the approval, the approval is a policy's, the rollout authorization policy is automatic, at least one node is already placed at the current revision, every retained node runs the current revision, and the approval covers every action in the plan. An uncovered plan waits for the operator rather than partly executing, a first placement always waits for Apply, and a retained node is observed but never repaired under this approval, because the placement policy decided the node set and not the content. A node-driven roster change no longer hides a policy approval behind a current combined one: a label or cordon change moves the node set without moving the combined approval's fingerprint, so the reconciler tries the policy approval first and falls back to the combined approval when the policy cannot run the whole plan. That fallback narrows the operator's blast to the placement's frozen target set instead of invalidating it, so a removal a policy approved no longer stops drift observation on the nodes that stay; the preview and the lightweight list and detail evaluators read the same narrowing, so no surface reports an approval the executor withholds or withholds one it will run. An operator's approval is never discarded by a policy decision. Demoting an application back to Inline takes the Git-era authority with it: the placement pointer is cleared and the rollout authorization policy returns to operator authorization, because neither was an operator's choice about Inline placement. An Inline Blueprint can reach the automatic rollout authorization the policy path requires from the control beside the placement policy. * fix(gitops): say when automatic placement waits for Apply A selector edit that moves one stateless node runs on the next tick, and Enforce holds a policy-approved plan until Apply. The docs described both as waiting, and the preview showed a pending plan with no reason. * fix(gitops): keep a cleared pin from automatic placement A pin clear left the new intent unpinned, so bounded auto could place the freed node without Confirm Apply. The decision now treats a clear as pin-driven when the standing approval was recorded on a pinned intent, or when a pin was written after an older approval and never confirmed. * fix(gitops): keep a detached snapshot off automatic placement Git-managed deploys stamp the Blueprint revision, and Git content never bumps it, so after Detach a retained node still looked current. Recording a new revision on Detach makes that stamp stale until Apply. * fix(gitops): hold Enforce plans that drop a mid-deploy repair A retained node that is correcting or deploying leaves the plan with no repair, so a policy approval could place an added node in Enforce. The mode itself now holds that plan until Apply, and the preview says so only when Enforce is the reason it is waiting. * fix(gitops): keep a pin chain and a re-armed policy from placing on their own Three authority gaps, all on the automatic placement path, found by the pre-merge audit. A pin that was set and cleared with no Apply between them read as no pin at all once a third intent followed: the intent before the current one was the clear, so the pin that moved the work was out of view and the freed node was placed without Apply. With no decomposed placement approval to anchor on there is no record of any confirmation, so the whole intent history now counts. A Confirm Apply mints the approval the decision anchors on from, and the next change is judged against that, so the wait ends at the next operator decision rather than never. Re-enabling a policy revived a decision the operator had taken back: execution stopped the moment the policy went, but the approval the policy wrote stayed on the row, so setting it back handed the next tick a decision made days earlier on evidence nobody had read since. Arming either policy again now withdraws what that policy decided, with the rollout authorization that only resolves through it. An operator's approval is untouched, the approval rows stay as the record of what was decided, the rollout generation stays so a fresh approval re-drives it, and the history row records the pointers that went with the policy. The refusals a withdrawal produces now read as what the backend checks: a node that has not reported, or that is not a node this Sencho knows about, is a different answer from a node that is not answering, and a withdrawal needs the node to have reported the workload it was running. * fix(gitops): clear the attention a policy placement left behind A Blueprint the placement policy placed a node for sat at Needs attention with a stale acknowledgement and a runtime drift, and neither Confirm Apply nor an API apply cleared it: the retained nodes' plan is observation-only, so nothing ever re-acknowledged them. Two causes, both fixed. The Inline freeze is now keyed on the compose digest rather than the intent revision, so a roster change no longer drops the accepted generation and re-points every placed target at a new generation id for the content it already runs. And the projection's identity test yields when both intent revisions resolve with the same compose digest and the current candidate still requires that node: a node the candidate dropped keeps its signal, because a withdrawal the plan is asking for is not a superseded acknowledgement. Two roster changes inside one tick also left both additions unplaced with no refusal recorded. The reconciler runs one approval and refuses a plan it does not cover whole, so the second approval named only its own addition while the plan still wanted the first. The blast now names every node the fleet does not hold, carrying the earlier decision's unlanded placement forward, while the decision still reads the approved set for its size and safety reasons and the evidence is read for every node the blast names. Removals stay on the approved set, because a withdrawal is judged from the observation of the workload that ran there. The Confirm rollout dialog no longer claims the fleet cannot change until it is confirmed when a configured policy has already approved the plan. * fix(gitops): write the placement blast in canonical order and name a withdrawn node again Two findings from the QA run of this PR, both now pinned by failing-first tests. The first: a roster move that adds a higher node id while withdrawing a lower one left the approval unwritable and the operator looking in the wrong place. The blast effect was assembled places-then-removals, while the decoder requires the node ids strictly increasing, so the write was refused as out of canonical order. The refusal reason then reported that refusal as `conflicting_operation`, so the operator was told an operation was in flight when none was, the plan read `reapproval_required`, and nothing executed. The effect is now sorted by node id, which is the order the decoder wants and carries no meaning of its own: the reconciler reads the effect as a map keyed by node id and the transition re-encodes it to canonical form anyway. Blast decode failures are also recorded as `malformed_evidence` rather than falling through to the conflict default, because a blast that did not decode is a data fault. The roster window that reaches it: the reconciler executes one approval per tick, so an addition approved but not yet landed is carried into the next approval's blast, which then names a placed node alongside the withdrawal. The second: a withdrawn node that the selector wants again produced no row in the plan at all. The tombstone holds deliberately, since only an explicit deploy re-opens a severed placement, but the node stayed online and labelled with nothing to say about why it was skipped, so the plan read as nothing to do. A new informational `skip_withdrawn` row now names what the operator has to do. It is excluded from the confirmable set and from both executor groups, so it authorizes nothing and cannot be confirmed into a mutate, and it yields to the status-precedence pass so an in-flight or guard row for the same node wins rather than leaving two explanations stacked on one node. Follow-up, not addressed here: the replay-guard refusal (`placement approval already recorded for this intent and candidate`) still maps to `conflicting_operation`, which is the same class of untruthful reason on the crash-retry path. It predates this change and is left for its own pass. Tests: a new case drives the add-then-withdraw window end to end and asserts the approval lands with `stateless_removal` and no refusal recorded; a second drives a severed target the selector wants again and asserts the informational row; a third pins that a severed node carrying a `withdrawing` row reports once, as the in-flight withdrawal. Backend: `npx tsc --noEmit`, `npm run lint` (0 errors), `npm test` (11013 passed; the single file-level failure is `git-transport-ssh.integration.test.ts`, which requires loopback port 22 free and fails identically on an unmodified tree because this host runs sshd on 22, freed in CI by `scripts/ci-free-loopback-ssh-port.sh`). Frontend: `npx tsc -b --noEmit`, `npm run lint` (0 errors). |
||
|
|
a6eaea0f54 |
feat(editor): create the stack .env from the editor on first save (#2097)
* feat(composeEditor): create .env file on demand when saved instead of relying on doing so via Files * fix(editor): scope .env creation to the editor and adopt the resolved path - PUT /:stackName/env creates the default .env only when the request carries create=1 and the stack exists; every other caller keeps the handled 404, so a Fleet Secrets push to a stack with no env file never creates one. - The create response returns the resolved path; the editor adopts it, so the next save targets the file directly instead of failing the allowed-file check. - Deep links to /env open the env tab on a stack with no env file. - Mobile always shows the compose/.env toggle and the creation notice. - Backend and frontend tests pin the create path, the canonical second save, the no-flag 404, the deep-link behavior, and the notice; docs, the guardrails tutorial, and the mobile screenshot are updated. * fix(editor): make the first env save exclusive and pin the confirmed overwrite Audit follow-up on the create-on-first-save path: - PUT /:stackName/env?create=1 now creates exclusively with open(..., 'wx'). An existing target answers the same 412 conflict the optimistic-concurrency path uses, with its current content and path, so a stale editor that still believes no env file exists can never overwrite one it has not read. - The editor adopts the conflict path, and its confirmed overwrite retries with create=1&force=1&file=<path>, pinned to the file the dialog named and re-validated against the allowlist. - With project env files configured, the create targets the first configured file (the one Compose reads), derived from the shared env resolver instead of a second copy of the fallback; a configured path that resolves outside the stack directory is refused with 409 rather than written. - The conflict read repeats the containment check and refuses to follow a link, so the conflict payload cannot be used to read outside the compose root. - A directory target maps to 409 instead of an opaque 500. - Tests cover the conflict instead of an overwrite, the forced retry, the pinned retry, the configured target with a stray .env untouched, the escaping config refused, force ignored on the non-create path, and the frontend conflict, decline, and fallback flows. - Docs restore the compose row's "Always available." note, describe the creation target and the no-overwrite guarantee, and make the notices generic. * fix(editor): guard the env create against older nodes and unknown state Audit follow-up on the create-on-first-save path: - The non-forced create request now carries If-Match: W/"0". This build's exclusive create ignores it; a node that predates create-on-save treats it as a normal conditional write, so an existing env file answers 412 with its content instead of being overwritten, and a missing file keeps the handled 404. - A failed stack load clears the previous stack's env state instead of leaving a stale "no env file" assertion that the next save could act on. - A failed env inventory or content read is tracked as unknown, shows a failure notice, locks the buffer, and refuses the save; it is no longer presented as an empty file. - Declining the create conflict on a node whose 412 names no path keeps the conflicted content and refreshes the inventory under the current load's signal; a failed refresh reports the failure instead of a false reload. - Save & Deploy and Save & Pull on an untouched empty env tab no longer create an empty file; the deploy proceeds. - The exclusive create opens with O_NOFOLLOW so a planted symlink cannot be followed, and a failed write's leftover is removed only when the path still names that same file (inode and size match). - Tests cover the older-node precondition, concurrent creates, a directory target, escaping and dangling symlinks, the write-failure cleanups, the unknown-state refusal, the conflict-retry copy, and the deploy skip. * fix(editor): leave create mode on older nodes and return 409 for path conflicts Audit follow-up: - A create-mode 200 without envPath is the normal response of a node that predates the envPath field. The editor now refreshes the env inventory in that case, so it leaves create mode and later saves are normal conditional writes with a fresh ETag instead of prompting for an overwrite every time. - The recovery refresh uses the load signal captured before the PUT, so a stack switch during the request aborts it and the new load's state is not repainted. - PUT /:stackName/env maps INVALID_PATH and SYMLINK_ESCAPE from the create path to 409, so a symlink at the target is a recoverable conflict instead of an opaque 500. - Docs: deep-links no longer says the created file is always the default .env. - Tests: the older-node create, confirm and overwrite sequence asserts the editor leaves create mode and the next save is a conditional write; a route test covers an in-root symlinked .env returning 409 without leaking content. - Comment cleanups from the audit. * fix(editor): do not paint a create response that lands after a stack switch Audit follow-up: - saveFile checks the load signal captured before the PUT (opLoadSignal) in the 412 and success paths: a response landing after the editor switched stacks now returns without adopting the old stack's envPath, ETag, or buffer state and without prompting, so the new stack's editor state stays intact. - Tests: two hook tests switch stacks mid-PUT, for both the success and the conflict response, and assert no adoption and no prompt. - Docs: node-compatibility no longer claims every older node detects the conflict (nodes before 0.87 cannot) and notes that the confirmed-overwrite prompt does not repeat. - Comment: createTarget wording matches the resolver's containment filter. * fix(editor): create missing parent directories for a configured env file Audit follow-up: - A configured project env file can point into a subdirectory that does not exist yet; the exclusive open failed with ENOENT and the route answered an opaque 500. The create now makes the missing parents inside the checked, stack-bounded path and retries the exclusive open once, so the first save creates the file Compose reads at its configured path. - The retry re-runs the containment check and still loses to EEXIST if another actor created the file meanwhile. - Tests: a service test for a nested missing-parent target and a route test that configures config/prod.env with no config directory, creates it, and saves again through the returned path. * fix(editor): report a vanished stack on env create instead of resurrecting it Audit follow-up: - Drop openExclusiveCreatingParents and its recursive mkdir. The state it served (a configured project env file in a missing subdirectory) cannot be produced by the API: PUT /project-env-files rejects names with "/" or "\" and requires the file to exist, so no product path reaches that branch. The only reachable ENOENT is a stack directory deleted between requireStackExists and the exclusive create; recreating it left the deleted stack visible only as a hidden folder holding the env content and blocked the stack name. - Restore the direct exclusive open and map ENOENT in the route catch to the handled 404 Stack not found, so the race is reported and nothing is written or recreated. - Replace the two tests for the unreachable nested target with regression tests: the service refuses a target under a missing stack directory and creates nothing; the route answers 404 Stack not found when the directory vanishes before the create (real service ENOENT path) and does not recreate it. * fix(editor): recover when the create conflict target is deleted mid-dialog QA follow-up: - The confirmed create retry pins the file the conflict named. When that file is deleted before the operator confirms, the route rejected the pinned path with a 400 because it was no longer a present env source, and the editor (which had already adopted the path) failed every later save the same way. The create branch now accepts the pinned path when it is the resolved create target, so the retry recreates the file; any other pinned path keeps the scoped rejection. - Declining a create conflict now reloads the env state instead of trusting the 412 payload: if the file was deleted meanwhile, the editor returns to create mode instead of adopting a path that no longer exists. - Save failures toast the server's error text (404 vanished stack, 409 unusable path, 413 too large) instead of the raw JSON envelope. - The save-refusal toast for an unreadable env state uses the same wording as the editor notice; the file list may have loaded while the content did not. - A viewer with no env file sees "This stack has no environment file yet." instead of an unexplained empty read-only tab; editors keep the create promise. - The route test for a vanished stack restores its spy in finally. --------- Co-authored-by: Anso <dev@anso.codes> |
||
|
|
02f6fc4946 |
chore(deps): bump the all-npm-frontend group (#2094)
Bumps the all-npm-frontend group in /frontend with 10 updates: | Package | From | To | | --- | --- | --- | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.48.0` | `1.52.0` | | [motion](https://github.com/motiondivision/motion) | `13.4.4` | `14.0.0` | | [react-day-picker](https://github.com/gpbl/react-day-picker/tree/HEAD/packages/react-day-picker) | `10.0.1` | `10.0.2` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.6.3` | `26.6.4` | | [eslint](https://github.com/eslint/eslint) | `10.11.0` | `10.12.0` | | [globals](https://github.com/sindresorhus/globals) | `17.12.0` | `17.13.0` | | [jsdom](https://github.com/jsdom/jsdom) | `30.1.1` | `30.1.2` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.70.1` | `8.71.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.3.1` | `8.3.2` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.2` | `5.0.3` | Updates `lucide-react` from 1.48.0 to 1.52.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.52.0/packages/lucide-react) Updates `motion` from 13.4.4 to 14.0.0 - [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md) - [Commits](https://github.com/motiondivision/motion/compare/v13.4.4...v14.0.0) Updates `react-day-picker` from 10.0.1 to 10.0.2 - [Release notes](https://github.com/gpbl/react-day-picker/releases) - [Changelog](https://github.com/gpbl/react-day-picker/blob/main/packages/react-day-picker/CHANGELOG.md) - [Commits](https://github.com/gpbl/react-day-picker/commits/v10.0.2/packages/react-day-picker) Updates `@types/node` from 26.6.3 to 26.6.4 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `eslint` from 10.11.0 to 10.12.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.11.0...v10.12.0) Updates `globals` from 17.12.0 to 17.13.0 - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v17.12.0...v17.13.0) Updates `jsdom` from 30.1.1 to 30.1.2 - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](https://github.com/jsdom/jsdom/compare/v30.1.1...v30.1.2) Updates `typescript-eslint` from 8.70.1 to 8.71.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.71.0/packages/typescript-eslint) Updates `vite` from 8.3.1 to 8.3.2 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.3.2/packages/vite) Updates `vitest` from 5.0.2 to 5.0.3 - [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/v5.0.3/packages/vitest) --- updated-dependencies: - dependency-name: lucide-react dependency-version: 1.52.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: motion dependency-version: 14.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-npm-frontend - dependency-name: react-day-picker dependency-version: 10.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@types/node" dependency-version: 26.6.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: eslint dependency-version: 10.12.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: globals dependency-version: 17.13.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: jsdom dependency-version: 30.1.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: typescript-eslint dependency-version: 8.71.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: vite dependency-version: 8.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: vitest dependency-version: 5.0.3 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> |
||
|
|
a54ebcc7e2 |
feat(fleet): separate the Fleet tabs into two sections, with a layout setting (#2092)
* feat(fleet): group the Fleet tabs into Observe and Operate, with a layout setting The Fleet tab strip was ten tabs in one row, wider than a 1280px window. The strip now sets the tabs under two labelled groups on one strip: Observe (Overview, Readiness, Map, Docker Labels) and Operate (Snapshots, Blueprints, Routing, Federation, Actions, Secrets). It scrolls sideways when the window is too narrow and keeps the tab you are on in view. Every tab still opens with one click and every tab keeps its value, so deep links and URL sync are unchanged. A new account preference, Settings, Appearance, Fleet, Fleet tab layout, picks how the strip is laid out: Grouped (default), Flat (the previous single row), or Compact (Overview, Readiness, Map, Blueprints and Actions, with the rest under a More menu that names the active hidden tab). It is saved to the account and synced like the other appearance preferences, defaults to Grouped for a document that lacks it, and is cleared with the rest of the preference cache when another account signs in. * refactor(fleet): drop the Observe and Operate labels and the old flat strip The group labels only lengthened the tab row, and the previous single-row layout is no longer needed. The Fleet tab strip is now one row: Overview, Readiness, Map and Docker Labels, a vertical separator, then Snapshots, Blueprints, Routing, Federation, Actions and Secrets. It still scrolls sideways when the window is too narrow and keeps the active tab in view. The Fleet tab layout setting now offers Flat (the default, this row) and Compact (five tabs with the rest under More). The retired grouped value reads as Flat. * test(fleet): raise the cross-tab storage event without the StorageEvent constructor The code quality scan flagged the two-argument StorageEvent calls as superfluous trailing arguments. The tests now build the event from a plain Event carrying the key and the new value, which is all the hook reads. |
||
|
|
d7e0ebc4e5 |
feat(fleet): draw the Map one node at a time as each answers (#2091)
* feat(fleet): draw the Map one node at a time as each answers The Map tab asked for the whole fleet in one request, so every node waited on the slowest one and a single unreachable node held the tab until its timeout. GET /api/fleet/dependency-map now takes an optional nodeId (positive integer; 400 if malformed, 404 if unknown) and answers for that one node in the same shape. The Map tab reads each node on its own, four at a time, and draws each slice as soon as it arrives, with "n of N nodes" beside Refresh while the rest are pending. A node that fails keeps its last slice and is named in the banner with its reason, and Retry in the banner reloads only the failed nodes. A node that never answers is given up on after 20 seconds so Refresh and Retry never stay locked behind it, and an answer that is not a map is reported as a failure instead of drawing nothing. When no node could be read the tab says so instead of calling the fleet empty. * test(fleet): compare the request origin, not a URL prefix, in the map filter tests CodeQL flagged two startsWith checks on a base URL as incomplete URL substring sanitization. The assertions now parse the request URL and compare its whole origin. |
||
|
|
fd5824c142 |
feat(fleet): add Cordon and Delete to the node details sheet (#2090)
* refactor(fleet): give each node one state and one action, and redesign the node details sheet The Fleet node card carried up to six chips (version, update, critical, cordoned, GitOps, integration image) and said "Node unreachable" three ways with no reason and no action. One model now reads a node's state for both surfaces: the loudest condition is the Answer, the rest are counted. The card shows the version as quiet text, one state chip with a +N for the others, and at most one action button, shown only when the session may run it. An offline node says why in one line (Pilot disconnected, last seen 12m ago, or Proxy unreachable, last contact 1h ago) with a Test connection button that runs in place, instead of a dimmed card. Stale update, GitOps and networking readings are no longer shown for an offline node, but a failed or running update stays visible with its Retry and Dismiss. The details sheet opens on an Overview that answers first, with a quiet row of the stages that apply and a collapsed Details section, plus Resources and Settings tabs. Its toolbar holds the object-level actions (Edit, Cordon or Uncordon, Delete) and the action for every other state the node is in, so no action is lost when a louder state is the Answer. Cordon confirmation and the connection test are shared by the card and the sheet. * refactor(fleet): keep the original node card and one-page sheet, add Cordon and Delete to the sheet The one-state-chip card and the tabbed details sheet are withdrawn. The node card is back to its original layout, where the only action button is the node update, and the details sheet is back to its single page. What stays: the details sheet toolbar now carries Edit node, Cordon node or Uncordon node, and Delete node (with View networking when the node has a networking signal), under the same permission rules as the card menu. The cordon confirmation is one shared component used by the card and the sheet, so the reason field, busy lock and failure handling are the same in both. The phone screen is unchanged. |
||
|
|
c925c7b887 |
refactor(fleet): put the Fleet masthead on the shared pattern and list nodes attention first (#2089)
The Fleet masthead was hand-built: its state word always read "The fleet", it showed a healthy tone with zero nodes while loading, and its "alerts" tile restated the critical-node count. It is now a PageMasthead hero with a FLEET kicker and a real verdict (Healthy, Degraded, Critical, plus Checking before the first overview, Unavailable if that first overview failed, and No nodes for an empty fleet). One detail line names the node count, offline and critical counts and sync age, and CPU, memory and containers sit in the metadata. The peak-CPU sub-line and the alerts tile are gone. Fleet nodes can now be sorted by Attention (offline, then critical, then name), which is the default for new browsers. The Local node stays pinned first in the grid. |
||
|
|
f38d4886d5 |
fix(fleet): report a gone Pilot as offline and bound the Fleet map reads (#2088)
A Pilot whose agent stopped kept reading "online" on Fleet forever with no stats, because pilot_last_seen was written only when a tunnel connected. The tunnel close now stamps it, and the overview treats a Pilot with no tunnel as online only for a 30s reconnect grace window. The dependency map and networking summary waited up to 15s per remote, so a half-open Pilot tunnel (agent frozen, not yet reaped, up to about 90s) held the Map tab for that long. Both now share an 8s per-remote budget, say "Timed out after 8s" instead of the runtime's abort text, and tell a body-read timeout apart from a malformed payload. A failed tunnel-down write no longer blocks the tunnel-down event. On the Fleet screen, an aborted overview request no longer clears the loading state of the request that replaced it (which flashed "No nodes configured"), the networking and GitOps requests have their own controllers so a poll does not cancel them, and polling pauses while the tab is hidden. The Overview skeleton waits for the busy delay, and the Map tab keeps its last result, revalidates when reopened, shows a layout-matched skeleton, names each unreachable node with its reason, and offers Retry. |
||
|
|
612c25a670 |
feat(gitops): complete the Git-managed Blueprint rollout path (#2084)
* fix(gitops): materialize frozen rollout targets on dispatch A Git-managed Blueprint rollout dispatched against its authorized frozen target set failed on every node except the source stack's own node with "Could not open deploy for node N: target not found". Only the Inline producer created a target row on first contact, and the Git adapter sent to nodes that had never received a Direct deploy. Create an empty target row for each frozen node that has none before the deploy queue runs, mirroring the Inline producer's deploy_start write. A node that no longer exists is skipped and still refused by name. An existing row is left untouched, so a tombstone, fence, or spent retry budget survives row creation. * feat(gitops): materialize and freeze the accepted generation for Git-managed rollouts A converted source keeps polling, but the Direct pipeline is refused for a claimed source, so nothing promoted a new generation into its applied directory and nothing resolved its artifact set. The rollout dispatch reads the applied materialization and authorization refuses an unresolved set, so a second commit could never be authorized. Promote the accepted generation's candidate into its applied directory on the hub (no writes to the live source stack) and resolve the artifact set from that materialization through the shared approved-content parser. The reconciler's artifact retry now re-runs both halves for a Git-managed Blueprint, so a transient failure heals. A generation that materializes more than one compose file is refused rather than resolved from a subset, and candidate and applied paths get the same shape validation the Direct promotion uses. * fix(gitops): rebind the rollout candidate to a newly accepted generation After a source-only change, sourceAccepted cleared the rollout authorization but kept the placement approval, while the candidate still named the previous generation and artifact. authorizationIngredients refuses a candidate whose binding disagrees with the application, so a second commit could never be authorized. Move the candidate's generation binding to the newly accepted generation and clear its artifact binding in the same transaction. The artifact binding is cleared rather than pointed at the acceptance's seed set because the freeze advances the application's set to the resolved one; authorization re-stamps both from that resolved set. Both acceptance entry points share the rebind, and the store method checks ownership and is idempotent. * fix(gitops): hold an authorized rollout whose dispatch is refused A refused dispatch after a successful authorization returned only a note on a 200 and left the projection reading rollout_queued with nothing an operator could act on. Hold the rollout through the same application pause the health executor uses, so the Answer carries the reason and Resume is the resolving verb. The hold is a no-op when no authorization is live and never overwrites an existing hold. It is placed by the authorize route, the automatic acceptance path, the resume path when the retry is refused again, and the startup reconstruction, with a fresh operation id each time so a repeated hold still writes history and announces itself. A hold that cannot be recorded is logged, never thrown into a dispatch that already answered. * test(gitops): cover a second source commit end to end Drive the audit's repro at the service level: a Git-managed Blueprint rollout to two frozen nodes, a second source commit accepted and prepared through the real materialize and freeze producers, re-authorization through the configured policy, and a second dispatch that deploys the new compose to both nodes. The test asserts the promoted materialization, the resolved artifact set, the minted authorization, the rebound candidate, the deployed compose bytes, and the per-node applied generation. * docs: document the Git-managed source update path Describe what a new commit does on a Git-managed Blueprint: acceptance materializes the generation and attempts to resolve its artifact identity (the reconciler retries an incomplete resolve), placement stays approved across a content-only change, and authorization starts the sequential rollout again, on its own under the automatic policy or held with the reason when preflight or artifact evidence is not ready. The Resume bullet records that a refused retry re-holds. The What's New blurb now describes the working chain instead of a blocked state. * fix(gitops): retry Git-managed preparation and classify dispatch holds A newly accepted generation whose preparation failed (a registry outage, or a crash between the acceptance commit and the materialize call) was stranded: re-accepting is refused, and the per-target artifact retry only looks at the generation a target already holds. The reconciler now runs an application-driven retry for every live Git-managed application, re-materializing and re-resolving the accepted generation and completing the automatic handoff once it resolves. A compose shape the authored-text parser cannot model is refused and remembered, and its reason is persisted as an evidence limitation the projection can show. Blocked dispatches now carry whether they are holdable. Lock contention, an already-recorded pause, a per-target pause, a superseded generation, and a suspended source are not: pausing on them stopped a healthy rollout another dispatch was advancing, and the per-target case needed two Resumes to undo one decision. The health rollout executor shares the classification. A converged Git-managed Blueprint no longer projects the Direct manifest cache as managed-project drift. * fix(gitops): clear system holds on acceptance and record their origin A system hold (a refused dispatch, a health fence) belonged to the rollout it was holding, but acceptance never cleared it: the next generation dispatched into "The rollout is paused." with a reason describing the superseded one, and before this change the next commit dispatched on its own. Holds now carry an origin. A system hold is cleared when a source acceptance supersedes its rollout, for every target mode, with a rollout_unpaused history row; an operator pause survives until the operator resumes. Health-executor and fence holds record the system origin too, so the same rule covers them. The docs and the accept-route note now describe the retry, the automatic handoff, and the unmodellable-compose refusal the way the code behaves. * fix(gitops): close the re-audit findings on holds and the retry The health executor now forwards the pause-already-on-record marker and the adapter marks the paused refusals with it, so a per-target pause is not escalated to an application hold on that path. Every blocked result declares whether it is holdable, and the Direct pipeline's exact-shape tests follow. The reconciler's preparation pass runs before the enabled check for a disabled-only fleet, uses the same durable retry gate as the per-target path, and is detached from the tick; the tests prove the isolation, the permanent-failure gate, and the non-blocking tick. The accept route's dispatch wait is bounded, the resolved-artifact gate lives in the one handoff, and the unmodellable-compose reason now reaches the operator in the caveat and the accept note. * test(gitops): use the fixture in the non-blocking tick test The test declared a fixture without reading it, which failed the lint gate on CI. The fixture is now authorized and the test asserts that the released durable refusal held the application, which also proves the detached preparation pass reached the dispatch. * fix(gitops): re-drive the automatic handoff and keep hold origins honest The reconciler's content pass now has a handoff arm: a resolved generation under the automatic policy with no live authorization (a slow accept, a re-enabled Blueprint) or with unacked targets (a transiently refused dispatch) is dispatched on the next pass. The warranted predicate keeps it off a settled, progressing, withdrawn, or not-yet-placed rollout, and an in-memory floor bounds a failure that records no evidence row to the artifact retry interval. The pause origin now comes from the refusal's kind: executor dispatch refusals are system holds that a new commit clears, and only real health decisions (a stop, pause, or rollback) carry the health origin. The accept route gates its dispatch half on stack:deploy while the acceptance stays stack:create, the inventory and the UI match, the unmodellable-compose copy no longer prescribes a fix for the wrong refusal, and its cause is capped. * fix(gitops): tighten the redrive guards and report both accept halves The warranted predicate now leaves a disabled Blueprint, a target with an operation in flight, a fenced target, and a stale open run under an observe policy alone, so the reconciler does not race a progressing rollout or re-drive a no-op. A refusal that did not hold the rollout is diagnostic rather than a per-tick warning. The accept route reports the preparation note and the withheld-deploy note together, the UI fails closed when the rollout policy is unreadable, and the rollback-fence clear is scoped to the live rollout generation. Per-application memory is pruned. * fix(gitops): detach a superseded rollout on acceptance and bound the redrive A source acceptance now always detaches the named rollout generation, marking it superseded only when it was not already, so an earlier Supersede or Rollback cannot leave the frozen strategy reading as moved on and disable the automatic handoff for every later commit. The warranted predicate is generation-aware and skips per-target pauses, and the handoff arm has an in-memory floor at the artifact retry interval and a concurrency cap, so a steady blocked preflight or a persistent refusal is not re-evaluated on every tick. A rollback fence blocks the hold clear whatever generation wrote it, matching the dispatch queue's behaviour. The accept route drops the deploy split: the configured Automatic policy is the authority for starting the rollout, as it is for the automatic source acceptance, so the acceptance stays `stack:create`, the UI gate returns to `stack:create`, and the inventory row says so. * fix(gitops): guard the preflight-drift supersede and pin the review tests The preflight-drift invalidation only records a supersede when the generation is not already marked, so a repeat drift does not append a duplicate history row. The accept-route test seeds the automatic policy it names, and the paused-target test asserts the predicate's decision rather than only that it ran. * chore: re-trigger CI after the Actions runner delay * fix(gitops): make the withdrawal check durable and refuse over an unfinished rollback The automatic re-drive predicate no longer trusts the rollout generation pointer for a withdrawal: it refuses while the latest rollout authorization recorded for the accepted source is superseded, so a placement re-approval cannot revive a rollout the operator withdrew. It also refuses to mint when a live target still carries a rollback_pending fence, whichever generation wrote it. The supersede recorder is idempotent, so repeat invalidations and re-approvals stop appending duplicate history rows; the hold clear and the predicate both ignore retired rows, which can never finish a rollback. The accept route joins the preparation note and the dispatch refusal again. * chore: refresh CI after the Actions outage * fix(gitops): mark an operator withdrawal explicitly and share the rollback gate A dedicated withdrawn_at on the rollout generation is written only by the operator's Supersede and Rollback, so the automatic policy can tell a deliberate withdrawal from a system supersede (a preflight drift, a placement invalidation, a re-authorization) and still re-drives after those. Legacy superseded rows backfill as withdrawn, the safe direction. The unfinished-rollback refusal moved into the shared handoff, so the accept route and the SourceController honour it too, with the reason logged; the predicate no longer carries its own copy. The accept note joins its parts as sentences. * fix(gitops): withdraw the latest authorization and hold over an unfinished rollback The operator's Supersede and Rollback now mark the latest rollout authorization recorded for the accepted source as withdrawn, not only the generation the pointer names, so a placement re-approval after a system supersede cannot leave the automatic policy free to redeploy what was rolled back. The unfinished-rollback refusal moved into the shared handoff as a holdable blocked refusal: it writes the reason on the application and the accept note shows it, instead of a silent skip. The rollback target set falls back to the fenced target's own generation, so the rollback can still be finished after an acceptance detached the pointer. The migration backfill is flag-tracked and transactional, the marker lookup is ordered by rowid, and the accept note joins its parts as sentences. * test(gitops): pin the rollback route, the withdrawal migration and the tick tests The rollback route gained its own tests: a placement pointer with an older authorization is withdrawn, and a fenced target keeps a resolvable target set after the pointer detached. The withdrawal backfill has a migration test covering the four states (fresh boot, column without flag, later system supersede, dropped column), and the latest-authorization lookup has a skewed-clock ordering test. The re-drive tests now use the settled tick-until helper instead of a single tick, so the handoff concurrency cap cannot defer the application under test. The rollback hold reason, the feature docs and the Resume note now say "finish it, then resume". * chore(deps): bump proxy-addr to 2.0.8 and source-map-js to 1.2.2 New advisories (GHSA-jqcg-44mw-7w3h, critical, and GHSA-68fv-2mgg-jv7q, high) require either a patched version or a VEX statement. Both patches are inside the existing dependency ranges, so the lockfiles move without any application change. * test(gitops): pin every withdrawal-migration state, including the flag gate The migration test now boots one database state per case: the column dropped without the flag, the column present without the flag (an earlier head, or a crash between the ALTER and the backfill), a fresh boot with no rows, and a later system supersede left untouched on the next boot. Reverting the flag logic to a column-presence check now fails three of the four, which is what the flag exists for. * fix(gitops): report the handoff outcome on accept and scope the docs claims The accept route now returns a skipped handoff's reason, so the operator can tell a started rollout from one waiting for authorization, paused, or held, and the UI prefixes the acceptance to the warning toast. The Resume reply under the Automatic policy no longer tells the operator to authorize when the reconciler will start the rollout itself. The feature docs scope the recovery-capture claim to Local and proxy nodes (Pilot capture is a tracked follow-up) and state that a Git-managed Blueprint skips the managed-project drift comparison by design. * fix(gitops): stop the accept note repeating the artifact reason When the preparation did not resolve the artifact identity, the accept note carried the preparation note and the handoff's unresolved-artifact skip, which say the same thing. The skip reason is now a shared constant and is suppressed when the preparation note already covers it; every other skip reason still reaches the operator. |
||
|
|
143f5a2005 |
feat(blueprints): open a Blueprint and the create dialog over the GitOps workplace (#2083)
From the GitOps workplace, opening a Blueprint or declaring a new one sent you to Fleet first and only then opened the sheet, so the list you came from, with its filters and scroll, was gone. The application sheet's Open Blueprint did the same, and Escape could not take you back. The Blueprint detail sheet and the create dialog now open over the workplace, the way a Direct application's Git source already does. They are the same sheets the Fleet Blueprints tab hosts, driven by one shared hook, so permissions, the create-then-review flow and label handling are identical in both places. A row's Open Blueprint, the application sheet's Open Blueprint (which hands its sheet over) and New Blueprint all go through it, and fall back to the Fleet Blueprints tab where the workplace cannot host them. The link back to the GitOps portfolio is left out of a sheet that already sits over it. Reading node labels is sequenced so an older answer never replaces a newer one, and a failed read says so instead of leaving label targets silently empty. |
||
|
|
8a9115f48c |
feat(blueprints): call the tab Blueprints, add a Needs attention filter and keep the catalog on screen (#2082)
The Fleet tab was called Deployments while every surface inside it said Blueprints, its empty state repeated a heading and offered two create buttons, and the populated catalog opened with a third naming of the same thing. The filter row had a Drifted chip that missed failed, conflicting, blocked and awaiting-confirmation Blueprints, and a tile showed only a coloured dot, never the state it stood for. Every refresh blanked the whole tab (closing an open sheet's context) behind a plain "Loading blueprints" line. The tab is now Blueprints. One header row carries the count and New Blueprint; the empty state is just its card with one create action. Needs attention replaces Drifted and covers failed, name conflict, evict blocked, awaiting confirmation, held repair, drift and a rollout that needs reconfirming; a tile names its strongest non-active state, and an empty filter offers Show all. The first load shows a skeleton shaped like the catalog, and only if it is slow. Later refreshes keep the catalog and any open sheet on screen; a failed refresh over a catalog shows a notice with Retry and the last list, and the error card is only for a tab with nothing to show. Overlapping refreshes are sequenced, and a failed refresh after creating a Blueprint no longer opens its sheet later. Docs and the e2e tab lookup follow the new name. |
||
|
|
d3fb532dfa |
feat(blueprints): give the Blueprint sheet one Answer and a shorter toolbar (#2080)
The Blueprint detail sheet opened on GitOps rollout and runtime stages that say nothing true about a Blueprint that is applied rather than rolled out: a healthy, fully deployed one showed an amber "rollout not executable" and "runtime never applied". A failed deploy was told five times, its Docker log pushed Re-apply and Withdraw off the right edge of the table, and the toolbar carried five buttons, three of them rare. An Inline Blueprint now leads with one status block built from its own deployments: in sync, failed, drifted, needs confirmation, reconciler off and so on, naming the affected nodes and offering the verb that resolves the loudest problem (Re-apply, Review rollout, Review state, Evict, Enable) only when this session may run it and the server would accept it. The GitOps status takes over when it holds a louder warning, and the source that did not speak is named in a quiet marker. Rollout "not executable" and runtime "never applied" are left out for a Blueprint that has no Git rollout; a paused rollout or a failed runtime still show. The toolbar is Apply now, Edit, Enable or Disable, and Delete. Convert to Git, Detach Git and Retire to Direct sit in the Content row next to the source they change, and a pinned Blueprint gets an Unpin button. A failed row shows the one line that says why, with the full output behind a control, and its action is named Re-apply to match the status block. |
||
|
|
7051673553 |
feat(blueprints): pin the create actions and show form errors beside the field (#2079)
The Blueprint create dialog kept Cancel and the primary action at the end of the scrolling body, so at common window heights the button was off screen, and a missing name or target only surfaced as a toast while the field it named was scrolled out of view. The create actions now live in the dialog's own footer and stay in view, with a short line saying whether the action adds node labels or deploys nothing yet. Name, compose and target problems show below their field, the name field takes focus, and each message clears as soon as that field is fixed. Reconciler enabled is a switch with a one-line consequence, the three stateful notices are one line, a failed compose analysis is logged and labelled instead of silently leaving a stale classification, and the default compose and drift card captions read in plain words. Deleting a Blueprint uses the shared destructive confirm with a busy state and keeps the typed-name check. |
||
|
|
03e2636e6a |
feat(blueprints): create node labels and see matched nodes from the Blueprint form (#2077)
The Blueprint form sent operators to Settings > Nodes whenever no node label existed, and closing it to do so discarded everything typed. It also showed the same labels in two rows (any / also all) with no list of the nodes a selector actually reaches. Targets now has one row of node labels with a Match any / Match all switch and a live "Matches N nodes" list. "+ Label" stages a new label on chosen nodes inside the form; the pending tags are listed and removable, and they count toward the match list immediately. Nothing is written to a node until the form is committed: Review rollout (or Create blueprint / Save changes where a rollout cannot be reviewed) saves the Blueprint, adds the staged labels through the existing node label endpoint, then opens the rollout preview. Save as draft stores the Blueprint only. Staged labels that the final selector does not use are never written. A Blueprint saved with both an any and an all list keeps showing two rows. Docs and the create-and-approve tutorial follow the new flow; two screenshots that no longer match any step are removed. |
||
|
|
6178bc6c4b |
fix(gitops): normalize Docker architecture names to OCI platform names (#2074)
* fix(gitops): normalize Docker architecture names to OCI platform names Docker reports the daemon host's `uname -m` spelling in `docker info` (`x86_64`, `aarch64`, `armv7l`), while registry index descriptors name platforms with OCI names (`amd64`, `arm64`, `arm`). The digest resolver compares the two by exact string, so GitOps artifact resolution failed on every Docker host with "no linux/x86_64 variant", and a Direct Git source apply with deploy stayed applied_not_deployed with "Private registry readiness could not be proven". Translate the architecture and OS at the read boundary, so the local and remote platform reads return OCI names. Tests cover the docker-info spellings, the Windows path, and the resolver's exact-match contract. * fix(gitops): tolerate legacy platform labels and drop the ARM guess An artifact set stored before the read translated docker-info architecture spellings keeps platform labels like linux/x86_64. The identity comparator and the evidence decoder now canonicalize both sides, so digest repair and the stale-probe membership check still find the approved child, including when the platform label comes from a leaf that has not been upgraded yet. The armv6l/armv7l/armv8l rows are removed: OCI distinguishes the arm variants and this code does not model the variant, so mapping them to `arm` could qualify a child built for a different ABI. Those hosts fail closed again. loongarch64 now maps to loong64. * fix(gitops): match a containerd node's index-only observation The containerd image store exposes only the image index digest locally (image Id, RepoDigests and descriptor all carry it), so the platform child digest is never visible on the node. The identity comparison required the child and reported every multi-arch deploy as runtime artifact drift even though the running image was the approved child. An observation that names no child of the expectation now proves the child through the frozen index digest and the platform the running image declares. An observation that names a recorded child must still name the approved one, and any digest that is neither the expected index nor a recorded child keeps the comparison fail-closed. The hub also translates the platform a leaf reports, so a leaf running before the normalization no longer fails every resolve, and a stored artifact row that cannot be decoded is logged instead of dropped silently. |
||
|
|
a1285754d1 |
fix(dashboard): keep the Stack Health name column readable at 1280 (#2073)
* fix(dashboard): keep the Stack Health name column readable at 1280 The GitOps grid minima (1004px this node, 1104px all nodes) did not count the grid's own horizontal padding, which border-box folds into min-width. At 1280 wide the table sat on that floor and the stack-name track was left 2px, so names were unreadable and the STACK and STATE headers ran together. Raise each GitOps minimum so it covers the fixed tracks, the gaps, the 40px row padding and a 160px stack-name floor: 1162px this node, 1266px all nodes. Below that the table scrolls horizontally with the name readable. * fix(dashboard): give the plain Stack Health grids the same name floor The non-GitOps grids carried the same floor-accounting defect as the GitOps ones: their min-widths did not count the grid's own horizontal padding. At the floor that left 2px (this node) and nothing (all nodes) for the stack name. Apply the same accounting, 998px this node and 1102px all nodes, each leaving a 160px stack-name track, and extend the regression test to all four grids. |
||
|
|
2f6c0fffe0 |
fix(preferences): migrate explicit UI preferences on first login after upgrade (#2072)
* fix(preferences): migrate explicit prefs on first login after upgrade An upgraded browser profile has explicit appearance and navigation preferences in localStorage but no owner marker, because the marker only exists once the server-backed sync has run. The first sync treated the missing marker as a foreign cache and wiped every preference key before hydration, so the absent-row migration then persisted defaults. Keep the cache whenever no marker exists: it is the account's migration source on first sync. Only a valid marker naming a different account wipes the cache before hydration. Signing out also keeps the cache and its marker, so a same-account re-login can still hydrate or migrate it; the next different account is wiped at claim time, before anything can render or migrate. * fix(preferences): reset live stores when a different account claims the browser Wiping the localStorage cache on an account switch left the theme and navigation stores holding the previous account's live values, so a brand-new account could render that look and then migrate it into its own server row. The foreign-cache claim now resets both stores to defaults, under the hydration guard, before hydration starts. A same-account login, a marker-less upgrade, and the tombstone and corrupt paths are unchanged. |
||
|
|
84f335097c |
refactor(dashboard): give the Home stack health table's GitOps state its own column (#2068)
The GitOps source-state tag (accepted, review required, pending update, and so on) sat beside the stack name and crowded it. It now has a GITOPS column after SOURCE, the name cell keeps just the name and the update icon, and clicking the tag still opens that stack in the GitOps portfolio. The column only appears while at least one stack in the table has a state the tag can show, so an instance with no Git-managed stacks keeps the table as it was. Stacks without a state read "--" in it, and a state this build cannot name no longer leaves an empty clickable button behind. |
||
|
|
e34948bb64 |
fix(gitops): announce local Git source changes on the channel GitOps views listen to (#2067)
Retry and Resume from the attention queue, and changes made in the Git source sheet over GitOps, raised the refresh signal as a bare event. The portfolio, the application sheet and the dashboard's Git source states only react to a signal tagged as a gitops change (and the dashboard also needs the node), so those views refreshed only if the server happened to push its own announcement. If it did not, the queue stayed stale after an action. Both places now announce through one helper that tags the change as gitops on the application's node, the same shape the server announces. |
||
|
|
67889e168f |
refactor(gitops): put the workplace masthead on the shared hero and drop the duplicate attention column (#2066)
The GitOps masthead is now the shared hero masthead with a GITOPS kicker. Its meta line carries how many nodes are reporting and when the data was updated, and reads "refreshing" while a refresh is in flight, so the floating refresh pill over the page is gone. A failed refresh keeps the last-known data with the same qualifier as before. Attention is one of the masthead counts. The application table no longer repeats attention in its own column: the attention list above it, the row tint and the state dot already carry it. The dot now has an accessible name (and a tooltip) giving the most urgent reason, so a row beyond the list's cap is still explained and screen readers get the reason too. Facet chips read the shared tone classes, and the portfolio load-error Retry is a regular button. |
||
|
|
2d89f13ef2 |
feat(gitops): resolve attention in place (#2065)
The attention queue now lists each application once, with its most urgent reason inline and a count of the rest, and every entry carries the step that resolves it where the operator already is. Retry and Resume on a Direct source run from the queue on the application's own node; Review update opens that stack's Git source sheet already pulled to its diff, so applying an update is two clicks from the GitOps page. A step the session may not run, or the server does not offer, opens the Git source sheet instead of a button that would be refused. The full-page application view is replaced on desktop by a sheet over the portfolio list, which stays mounted underneath, so closing it lands back on the same filters and page. A Direct application is its stack's Git source, so a row or a link that names one opens that sheet and never a second read-only copy. Blueprint applications keep their authority and rollout controls in their own sheet. The whole table row is now clickable; the actions menu is unchanged. The Retry, Resume and Suspend request now lives in one shared handler used by the sheet and the queue, and names the stack in its toasts. The phone screen keeps its application page. |
||
|
|
ee62a449b2 |
refactor(gitops): organize the Git source sheet into tabs and make editing deliberate (#2064)
The sheet was one long scroll with the repository form always open and a toolbar of five buttons. A linked source now has tabs: - Overview: the status block, with Review update, Retry, or Resume in it. - Source: a read-only summary of the configuration, an Edit button into the form (with Cancel), Adopt onto Blueprint, and the manifest. - Automation: the apply behavior, Suspend or Resume, and the provider hooks. - Secrets. - Drift, only while the application has drift. An unlinked stack gets no tabs, only the form. The toolbar keeps Pull now and Detach (named the same as its confirmation); Update appears only while something differs from what is stored, so it never sits there doing nothing. Reading and refreshing are more careful. A failed read now says so with a Retry instead of presenting an empty "link a source" form; a change that was saved but could not be re-read keeps the linked source on screen; a pull, apply, suspend, or resume refreshes the status without discarding a change that is still being made; and a slow answer for a stack the sheet has left can no longer overwrite the one it shows. Detaching empties the form instead of leaving the removed source prefilled. Docs, the Git sources page, and the two end-to-end specs that drive the sheet are updated. |
||
|
|
b689733447 |
refactor(gitops): one layered status across the Drift tab, Git source sheet, and application view (#2063)
* refactor(gitops): one layered status across the Drift tab, Git source sheet, and application view The GitOps state used to be a stack of tinted cards on five surfaces: a posture card, approval chips, a card per facet, a card per target, and a caveats box, restating the same state several times. Each surface now shows one status: a single answer (with its resolving button, such as Review update or Retry), a quiet row naming each stage that applies, and an Evidence control holding recorded authority, policy lines, targets, and anything Sencho could not prove. - Stages that say nothing about an application (unbound placement, an absent rollout) are left out of the row instead of printed as "n/a". - Evidence is never silently lost: the answer carries a marker for partial evidence, unreached nodes, and unproven items, and every attention reason is kept, the first in the answer and the rest in the evidence. - The Git source sheet's Status section drops from about 450px to about 170px, and its separate source-state and last-applied rows go; the header now says "never applied" until something is. - The application view no longer shows the posture twice. Adds a domain-free StatusPath primitive, a pure status model, and a GitOpsStatus composer; retires the facet-card and approval-chip components and keeps the policy lines. Docs, the tutorial, and the two affected screenshots are updated. * test(gitops): open the status evidence in the digest drift spec The per-service digests now sit under the Evidence control of the GitOps status, so the spec opens it before looking for them. |
||
|
|
bba9d30204 |
feat(gitops): open the Git source in place and animate Pull (#2062)
Opening a Direct application's Git source from the GitOps portfolio (attention queue, row menu, application header) now opens the Git Source sheet over the portfolio instead of switching to the stack editor. The sheet targets the application's own node, so reads, pulls, applies, provider hooks, repository secrets, and the manifest all go to that node even when another node is active. Changes made in the sheet refresh the portfolio and the open application view. Adopt onto Blueprint is offered only when that node is the hub, where adoption runs. Sheet toolbar actions gain a pending state: the button locks at once and, once the work outlasts the busy delay, its own icon spins. Pull now and Save use it, so the refresh circle turns while a pull runs. |
||
|
|
c0b5708dd7 |
fix(gitops): date a recorded placement refusal instead of presenting it as current (#2042)
A placement decision that refuses records its reason once, and the reason is cleared only when an operator approves the placement, edits the policy, or a later change re-decides. A decision runs only when a placement change is pending, so a refusal recorded because an operation was in flight outlives that operation and nothing corrects it on its own. The facet card rendered the reason in the present tense. An operator arriving after a deploy finished was told the system was withholding a placement because a conflict was in flight, and had to work out that the conflict was over. The record is kept and the moment it was made is shown. The timestamp was already stored and already projected as the decision's own date; the frontend carried the field and rendered nothing. Re-deciding instead would need a trigger that does not exist today, and would let an automatic approval land at a moment the operator is not watching, which is a larger claim than the evidence supports. Both tests fail if the behavior is reverted. The backend case drives the real transitions: an operation in flight, a refusal recorded for it, the operation finishing, and the projection still reporting the decline carrying the moment it was recorded rather than the moment it was read. Verified against a running instance at desktop and phone width: the card carries the date in both, and the only desktop delta is the one added line. |