Commit Graph

1995 Commits

Author SHA1 Message Date
dependabot[bot] 60fcfe54d3 chore(deps): bump the all-actions group across 1 directory with 4 updates (#1964)
Bumps the all-actions group with 4 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/autobuild` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/upload-sarif` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-23 18:27:01 -04:00
dependabot[bot] 38959e1ea8 chore(deps-dev): bump the all-npm-root group with 2 updates (#1961)
Bumps the all-npm-root group with 2 updates: [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) and [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional).


Updates `@commitlint/cli` from 21.2.2 to 21.2.3
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.2.3/@commitlint/cli)

Updates `@commitlint/config-conventional` from 21.2.2 to 21.2.3
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.2.3/@commitlint/config-conventional)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 21.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-root
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 21.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-root
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-23 18:26:57 -04:00
dependabot[bot] e5d256f8da chore(deps): bump the all-npm-backend group in /backend with 7 updates (#1963)
Bumps the all-npm-backend group in /backend with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [ldapts](https://github.com/ldapts/ldapts) | `9.0.0` | `9.2.0` |
| [systeminformation](https://github.com/sebhildebrandt/systeminformation) | `5.33.10` | `5.33.13` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.5.1` | `26.6.2` |
| [eslint](https://github.com/eslint/eslint) | `10.10.0` | `10.11.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.0` | `5.0.1` |
| [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1132.0` | `3.1136.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1132.0` | `3.1136.0` |


Updates `ldapts` from 9.0.0 to 9.2.0
- [Release notes](https://github.com/ldapts/ldapts/releases)
- [Changelog](https://github.com/ldapts/ldapts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ldapts/ldapts/compare/v9.0.0...v9.2.0)

Updates `systeminformation` from 5.33.10 to 5.33.13
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.33.10...v5.33.13)

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

Updates `eslint` from 10.10.0 to 10.11.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.10.0...v10.11.0)

Updates `vitest` from 5.0.0 to 5.0.1
- [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.1/packages/vitest)

Updates `@aws-sdk/client-ecr` from 3.1132.0 to 3.1136.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-ecr/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1136.0/clients/client-ecr)

Updates `@aws-sdk/client-s3` from 3.1132.0 to 3.1136.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1136.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: ldapts
  dependency-version: 9.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: systeminformation
  dependency-version: 5.33.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: "@types/node"
  dependency-version: 26.6.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: eslint
  dependency-version: 10.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: vitest
  dependency-version: 5.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-ecr"
  dependency-version: 3.1136.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1136.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-23 13:20:34 -04:00
Anso 7767fd6f93 fix(gitops): name the unresolved artifact identity and drop the stale source report (#1960)
The operator source-accept route seeds an unresolved artifact set and no
producer resolves one for a Git-managed Blueprint, so authorization refused
with a registry-worded message that named the wrong cause. Refuse before
evaluating registry readiness and report the artifact identity, on both the
route and the placement facet, through one shared predicate.

Placement also reported source acceptance pending from a rollout candidate
bound to the previous authorized generation, which no generation is waiting
on. Only a live staged generation counts as that decision now.

Source acceptance stays available while a Blueprint is disabled, matching the
backend, and the dispatch-failure note no longer points at a surface without a
retry control.
2026-09-23 05:18:58 +00:00
Anso b27fbeeb46 feat(gitops): add decomposed approval actions for Git-managed Blueprints (#1959)
* feat(gitops): add decomposed approval actions for Git-managed Blueprints

Source acceptance, placement approval, and rollout authorization are now
writable as separate operator actions under /api/gitops/applications/:id.
Placement approval re-runs the Blueprint preview and binds the reviewed plan
(409 with a fresh preview on drift); rollout authorization evaluates registry
preflight, records operator authority, and dispatches the accepted generation
through the shared boundary. The placement facet reports a Git-managed
candidate with no placement approval as placement review pending, and a
candidate newer than the accepted generation as source acceptance pending.

* feat(gitops): surface the outstanding authority step on the app view and sheet

The GitOps application view and the Blueprint sheet's GitOps section now offer
the outstanding authority step as one action: Accept source, Review and approve
placement, or Authorize rollout. Placement review opens the rollout preview,
which records the placement approval for a Git-managed Blueprint instead of an
inline apply, so the composed source and artifact evidence is reviewed there.

* docs(gitops): document the decomposed approval actions

The Blueprint page describes where each step appears, what placement approval
binds, and how a blocked preflight or a refused target start reads; the GitOps
portfolio page notes the action in the application view.
2026-09-23 04:26:23 +00:00
Anso a881e425f8 feat(gitops): compose the authority evidence into the Blueprint rollout preview (#1957)
The Blueprint rollout preview now embeds the canonical GitOps revision
projection and binds the confirmation to a digest of the authority evidence
it displays: the recorded approval refs, the rollout generation, and the
source, artifact, placement and rollout facet identities. Apply refuses with
PREVIEW_STALE unless the intent fingerprint and the evidence digest both
still match, so an acceptance, artifact identity, placement approval,
preflight result or rollout authorization that moved under the preview
cannot execute.

The dialog renders the composed evidence through the same chips, cards and
per-target rendering the GitOps application view uses, keeps the legacy
combined approval separate from the decomposed steps, and refreshes or
closes on a stale refusal instead of re-arming a preview the server already
rejected. Volatile observations (timestamps, retry counters, per-target
runtime and health readings) are excluded from the digest so a steady-state
application does not read as permanently stale.
2026-09-23 03:11:28 +00:00
Anso 4453ca552e fix(e2e): ride out the shared API limiter in the sidebar truncation spec (#1958)
The spec shares the app's per-minute API rate limiter with every other spec
in the suite, so a late-run burst can answer 429. Its setup treated that as a
hard failure, and a rate-limited bootstrap leaves the shell without its data
and does not recover on its own, so the retries then failed waiting for the
dashboard.

Retry the login and dashboard readiness, the stack creates (only a 429 is
retried; any other status still fails on the first answer) and the post-setup
reload, waiting out the window between attempts. The describe gets a longer
test budget so a saturated minute cannot time the retries out. The app's
rate-limit behavior is unchanged.
2026-09-23 02:30:01 +00:00
Anso 46937e7db1 feat(gitops): open each portfolio application in a read-only application view (#1956)
* test(gitops): cover every pending approval trigger and guard liveSourceFacet

Parametrize the approval chip tests over each pending and not-applicable
trigger status, and give liveSourceFacet the same missing-facets guard its
sibling facet readers already have.

* feat(gitops): open each portfolio application in a read-only application view

Portfolio rows now drill into a single-application view inside the GitOps
workplace, read from the hub-owned application endpoint: identity, attention
reasons, decomposed authority chips, the source, artifact, placement and
rollout facets, every target, classified drift, and caveats. The view hands
off to the owning surface (stack Git panel or Blueprint deployments) for any
action. The open application lives in a query parameter the GitOps view
owns, so the view is linkable and Back returns to the filtered list. The
phone screen reuses the same view.

* test(gitops): cover the application view, its data hook, and navigation

Also keeps a facet or target whose status this build does not recognize
visible as an explicit unrecognized state in the application view, instead
of dropping the card, and adds first render coverage for the placement and
rollout badge arms.

* fix(gitops): classify application read failures precisely and keep the view linkable

A node too old to answer, a malformed link, and an unreadable application
now read as three different states; retry stays available where a later
answer can differ. A successful answer the view cannot read is reported
instead of rendering blank, unknown target modes, postures and artifact
kinds render as unrecognized, and a failed refresh keeps a retry beside the
last-known banner. A search debounce that fires after a row was opened no
longer replaces the application URL. Documents the application view.

* fix(gitops): return to the portfolio from GitOps navigation and keep phone shell actions

Selecting GitOps in the navigation, or following a contextual GitOps
indicator, while an application view is open now returns to the list
instead of doing nothing. The phone application view keeps the shell
actions (notifications, more menu) in its top bar.

* refactor(gitops): tidy the application view navigation and evidence line

Narrows two hand-off helpers to module scope, reads the application id with
a plain URLSearchParams, folds a duplicated window check, and moves the
partial-evidence sentence into a small helper.

* test(gitops): pin the liveSourceFacet guard and the view's unrecognized and error states

* docs(gitops): show the application view on the portfolio page
2026-09-22 18:36:09 -04:00
Anso 85f5246445 feat(gitops): add the GitOps portfolio workplace (#1954)
* feat(gitops): add the hub-owned GitOps portfolio read API

Adds the canonical aggregate read surface behind the GitOps workplace:

- GET /api/gitops/applications: one bounded response covering Direct
  applications from the hub and every reachable node, plus hub-owned
  Blueprint applications. Remote legs probe each node's git-sources with
  a bounded timeout, rewrite node identity into hub numbering, and
  re-authorize every row against the calling user; unreachable or
  unsupported nodes become named coverage entries rather than vanishing
  rows.
- GET /api/gitops/applications/🆔 the row plus the canonical
  projection, the shared single-application contract (remote ids resolve
  through a single-node probe; legacy ids resolve by stack name).
- Server-owned attention classification (services/gitops/attention.ts):
  a closed reason union with failure/pending tone, computed once from
  canonical facets, so no client re-derives "needs attention".
- Per-application posture (failed/attention/in_progress/converged/
  converged_qualified/unknown) from the same facets; qualified
  convergence never claims exactness, and an unrecognized status from a
  newer node is reported as unknown evidence instead of being
  reinterpreted.
- The surface is Community and hub-only: the prefix is listed in
  HUB_ONLY_PREFIXES and node names are withheld from callers without
  node:read.

* feat(gitops): add the GitOps workplace view and phone screen

A first-class GitOps destination next to Fleet (launcher, command
palette, direct URL /nodes/<hub>/gitops, hub-only via HUB_ONLY_VIEWS):

- Portfolio masthead: verdict word, application/attention/drifted/
  pending/converged counts, and an honesty line that names unreachable
  or too-old nodes and says when a failed refresh left last-known data
  on screen.
- Attention queue: one entry per canonical reason with the explanation
  inline, failures sorted ahead of pending decisions, independent of the
  list filters so narrowing the table cannot hide an exception.
- Application table with server-side search, filters (attention, target
  mode, node involvement, drift class, evidence), cursor pagination, and
  drill-down hand-offs: Direct rows open the stack's Git source panel on
  the owning node, Blueprint rows land on Fleet deployments.
- Bespoke phone screen on the shared data hook: masthead-led, chip
  filters, inline attention reasons, >=44px targets.
- Refresh is event-driven off the gitops invalidate channel (debounced),
  with URL-synced filters that survive reloads; the router no longer
  strips query parameters owned by the view.
- Dashboard badges and the sidebar pending indicator link into the
  workplace through one shared navigate helper.

* docs(gitops): document the portfolio workplace

Adds the operator page for the GitOps portfolio (what the page answers,
how to read the masthead and queue, filters, freshness honesty) with a
screenshot, and registers it in the docs navigation.

* fix(preferences): accept the gitops destination as a quick-link id

The GitOps nav entry is quick-link eligible, but the backend enum that
validates saved quick-link preferences still listed only the previous
ids, so the preference-parity pretest rejected the value-set drift.

* chore(gitops): drop imports orphaned by the review pass

Three dead imports left behind when the aggregator and route code moved
during review: an unused test import, and two unused type imports.

* refactor(gitops): rebuild the workplace on the shared page patterns

The portfolio page was visually drifting from the rest of the app. It now
uses the same building blocks as the Dashboard, Fleet, and Security pages:

- Masthead rebuilt on the Dashboard/Fleet recipe: beveled card with the
  accent tint wash, a rail that shimmers while the data is live, the state
  word with a mono meta line, stat tiles (applications, pending, drifted,
  converged with a qualified sub-line), the attention count in the alerts
  slot, and a live ticker so "updated Xs ago" keeps moving. A failed
  refresh reuses the Dashboard's stale chip.
- Filter row matches the Security Images and Fleet tabs: the collapsible
  search affordance (square outline button expanding into a focused input)
  plus flat combobox filters on one wrapping row, replacing the bespoke
  chip set.
- Table rebuilt on the shared table shell: beveled card, ScrollArea so the
  scrolling stays inside the table, tracked-mono header cells, hover-tinted
  rows with the application name as the drill-down target, and the
  pagination footer in the standard ghost-button style.
- The e2e spec drives the new control shapes.

* test(nav): poll for the settled Navigate panel size

The panel-size assertion read scrollHeight/clientHeight once, immediately
after the panel opened. The popper's available-height variable is applied
after opening, so a single read can observe the pre-positioning clamp and
report a settled panel as clipped; on CI this failed intermittently while
the same check passes consistently once the layout settles. Poll until
the metrics stabilize, which is what the assertion actually means.
2026-09-22 15:52:05 -04:00
Anso 6345e695c4 fix(ui): wrap modal titles on word boundaries (#1950)
Modal headers split long titles at arbitrary character boundaries,
rendering fragments like "Prune Sencho-manage" / "d images". HeaderShell,
which backs all four modal header variants, applied break-all (breaks
between any two characters) plus line-clamp-3 (could hide
destructive-confirm context).

Replace break-all with break-words (normal words wrap only on word
boundaries; a long unbroken token still breaks as a last resort, so no
horizontal overflow) and add text-balance (multi-line titles wrap into
balanced lines, eliminating the orphaned single-letter second line).
Remove line-clamp-3: titles are short enough that clamping only risked
hiding context, and the dialog container caps at max-h-[85dvh] with a
scrolling body, so full titles cannot push actions off-screen.

Covered by class-contract tests across all four header variants and a
Playwright spec asserting computed wrapping styles, dialog containment,
and balanced line wrapping at the confirm-dialog geometry.
2026-09-22 14:37:21 -04:00
Anso 5981355c27 feat(gitops): render decomposed approval authority and placement/rollout state (#1955)
Add placement and rollout facet state maps with lookup views for
wire statuses from newer nodes, a presentational approval-chips
component that reads granted steps only from recorded refs and pending
steps only from facet statuses, and render-only wiring in the blueprint
detail, git source, and drift panels. Preflight-blocked placements now
show the redacted server reason on all three surfaces. No endpoints,
no mutations, no approval-flow changes.
2026-09-22 13:15:22 -04:00
Anso 489e1a9a9a feat(blueprints): detect and enforce runtime digest drift (#1949)
* feat(blueprints): detect and enforce runtime digest drift

Compare frozen expected artifact identity to live container digests so
tag movement surfaces as drift, and Enforce repairs with digest pins
(--pull never) without rewriting authored compose. Unreachable or
incomplete observation stays unverified and never auto-corrects.

* fix(security): keep digest overlay in tmpdir and sanitize observation logs

Write Compose digest-pin overlays under os.tmpdir() instead of the stack
directory so path expressions are not stack-name tainted, and log runtime
observation failures with a fixed format string plus sanitized stack name.

* fix(security): create digest overlay via exclusive mkdtemp

Avoid CodeQL insecure-temporary-file by writing the Compose digest-pin
overlay into an exclusive mkdtemp directory with wx create, then removing
the whole directory after deploy.

* fix: compare Blueprint digest drift by per-target platform identity

Enforce now routes container repair separately from digest pins, keeps
compose.override.yml on overlay deploys, and fails closed when the
target platform is unknown.

* test: expect platformVariants on single-platform digest resolve

* test: drop unused seedApp binding in digest-repair suite

* fix: drop redundant platformLabel nullish coalesce after guard

* fix(blueprints): accept override-only services in apply-local pin guard

* fix(blueprints): freeze and repair remote digests from the leaf

Hub digest freeze and Enforce repair now fetch the target node's rendered
Compose model and Docker platform over the authenticated proxy, so a remote
compose_dir string is never treated as a hub-local path. apply-local pin
validation runs after write against the rendered multi-file model.
2026-09-20 20:26:45 -04:00
Anso 2baf4e32ee fix(notifications): redact webhook URLs on channel reads (#1951)
Discord, Slack, generic webhook, and ntfy destinations embed tokens in
the URL. Return origin-only placeholders on list responses so any
authenticated reader cannot reuse those credentials outside Sencho.
2026-09-20 12:54:21 -04:00
Anso 23baec2a7d fix(gitops): prune provider webhook endpoints when a git source or stack is removed (#1940)
* fix(gitops): prune provider webhook endpoints when a git source or stack is removed

Detaching a git source, deleting its stack, or deleting a single webhook
endpoint left the git_provider_endpoints rows (which hold encrypted
secrets) and their git_provider_deliveries history behind as orphans.
FK enforcement is off, so the schema's ON DELETE CASCADE never fires;
delete the deliveries explicitly, then the endpoints, transactionally,
at all three removal points.

* test(gitops): assert provider-hook rotate does not prune endpoints

Pins the rotate path as update-only so a future prune regression cannot
quietly delete endpoints or deliveries when rotating a webhook secret.
2026-09-19 21:49:58 -04:00
Anso 2fb2c9116c feat(gitops): block Blueprint place until private registry targets are ready (#1946)
* feat(gitops): block Blueprint place until private registry targets are ready

Evaluate per-target registry readiness before minting rollout authorization,
persist blocked and ready evidence for derive, remint on fingerprint drift,
and refuse Direct deploy when private hosts cannot be proven ready.

* fix(gitops): let Direct apply tests override registry preflight

Unresolved artifact fixtures were failing closed before Compose in
git-source-service tests. Move readiness test hooks onto the evaluator
module and override evaluation for Direct suites that are not about
registry readiness.

* fix(gitops): stub registry preflight on apply-recovery mocks

The Direct apply recovery suite mocks GitOpsTransitions without the new
preflight methods, so deploy-after-apply crashed before Compose. Stub the
transitions and override the evaluator for non-registry fixtures.

* fix(gitops): clear code-quality findings on preflight serialization

Use a token for inflight evaluation cleanup instead of comparing promises, and drop a redundant summary guard already narrowed by the Blueprint detail branch.

* fix(gitops): distinguish unevaluated registry preflight from a failed probe

Surface a dedicated reason when placement has no stored evidence yet, document the application-scoped inbound envelope host set, and note the first-run vs failed-evaluation distinction in operator docs.
2026-09-19 20:54:55 -04:00
Anso 591b06220b feat(registry): seal private-registry credentials for HTTP delivery (#1945)
* feat(registry): seal private-registry credentials for HTTP delivery

Pin each remote node's X25519 sealing key on first use and deliver sealed
auths so private-image Compose ops work without a confidential transport hop.

* fix(registry): avoid TOCTOU when loading the sealing key file

Read and exclusive-create the key without existsSync races so CodeQL
js/file-system-race no longer flags the seal helper or its unit test.

* fix(registry): read sealing key test fixture through one fd

Avoid a check-then-read race in the unit test that CodeQL still flagged
after the helper-side exclusive-create fix.
2026-09-19 16:07:36 -04:00
Anso a53fc42b86 fix(catalog): use js-yaml namespace import for ESM v5 (#1942)
js-yaml 5 removed the ESM default export, so the website-catalog scripts
failed Catalog Drift Check with a SyntaxError on import.
2026-09-19 15:12:26 -04:00
Anso 23d232b7bb chore: ignore local research/ and .opencode/ directories (#1944)
Keep competitor research clones and OpenCode tooling out of the tracked tree.
2026-09-19 15:11:49 -04:00
Anso 9cbb5f8ceb feat(gitops): authorize Git-managed Blueprint rollout generations
Mint rollout_authorization with preflight fingerprinting, place from accepted generation materialization (never snapshot YAML), and project placement/rollout derive facets. Restart reconstructs remaining sequential targets without replaying acked ones under the live auth ref.
2026-09-19 14:04:26 -04:00
Anso c02dfa4fdd feat: listen with HTTPS so Pilot credential delivery stays confidential (#1943)
* feat: listen with HTTPS so Pilot credential delivery stays confidential

When the control instance is not behind a reverse proxy, cert and key files
opt the process into an HTTPS listener so Pilot tunnels are encrypted. Enrollment
emits an https URL and an optional CA bind-mount. Pilot agents ignore those
cert files so loopback replay stays HTTP.

* fix: verify native TLS health probes against the public URL

Keep certificate verification on for the loopback HEALTHCHECK. Dial 127.0.0.1, check identity against SENCHO_PUBLIC_URL, and trust SENCHO_TLS_CA_FILE when set.

* fix: trust HEALTHCHECK CA via the process store, not the request

Private-issuer PEM is added with tls.setDefaultCACertificates so the loopback HTTPS probe never puts file bytes into https.get options.
2026-09-19 13:51:27 -04:00
Anso 20d0db4691 feat(gitops): add rollout generations and Inline placement approval
Add immutable gitops_rollout_generations and mint placement_approval on Inline Confirm Apply with a legacy_inline generation that freezes the intent strategy. Cut the reconciler over to placementEffectCompatible, migrate existing Inline apps with non-authoritative legacy_combined, and keep Git-managed place blocked until the follow-up authorization PR.
2026-09-19 13:30:18 -04:00
Anso 39e0af9fa7 feat(registry): Pilot private-registry parity - target facts, hub authority, auto-update (#1930)
* feat(registry): Pilot private-registry parity — target facts, hub authority, auto-update

- Target facts contract (AUD-14, AUD-24): remote image-update observation tokens
  with 5-min TTL, one-use replay, sequential per-target execution
- Hub check authority and storage (AUD-11, AUD-21): overlay status endpoint,
  dual-scanner ownership matrix (hub vs target), independent enabled states
- Checked auto-update coordinator (AUD-22, AUD-28): awaited post-mutation
  verification, fleet recheck only on confirmed success, stale-observation refusal
- Registry transport safety (AUD-29, AUD-33): allowlisted public HTTPS realms,
  redirect-safety, no credential leak on blocked/unsafe addresses
- Frontend consumers: UpdatesSection dual-scanner controls, useImageUpdates hook,
  Security OverviewTab target recheck, Masthead stats integration
- Docs: private-registries.mdx updated with exact-proof delivery, capability
  requirements, and troubleshooting for transport, credential, and capability
  mismatches

* test: seed facts API token via shared test helper

The image-update facts boundary test inlined the production token-hash
construction (generateApiToken plus sha256) to seed its full-admin token
row, which re-triggered the recurring js/insufficient-password-hash
finding for test files that hash API tokens. Delegate to the existing
API token test helper instead: it mirrors the production construction
and its location already carries the documented false-positive
determination.

* fix(security): sanitize auto-update failure logs against injection

Strip control characters from stack names and error messages before
console.error so registry-sourced strings cannot forge log lines.

* fix(security): clear remaining CodeQL log and path injection alerts

Sanitize failure logs across registry delivery, safe-probe, fleet
restore, webhooks, and scheduler. Retarget authoredComposeArgs path
barriers to the compose root so CodeQL credits the containment check.
2026-09-19 08:26:05 -04:00
Anso 9d0dbebbfa feat(gitops): SOPS/age repository secrets without durable plaintext (#1935)
* feat(gitops): SOPS/age repository secrets without durable plaintext

Add age-only SOPS detection, node-scoped identity management, and a short-lived
decrypt overlay for Git validate, apply, deploy, and recovery paths. Ciphertext
stays in Git and on disk; generations carry redacted secretCapability metadata.

* fix(gitops): repair SOPS route and GitSourcePanel test failures

Mock GitSourceSecretsSection in panel tests and align SOPS identity route tests with viewer auth, upsert mocking, and application scoping.

* fix(gitops): repair deploy guard, schema test, and deploy-retry manifest

Use overlayDir only for env blocking during Git overlay deploys, register
gitops_sops_identities in the schema inventory test, and rebuild manifest
from revalidation when deploy retry runs before on-disk manifest exists.

* fix(gitops): clear unused imports flagged by ESLint

Remove an unused SecretCapability type import and stop destructuring
destroyGitOpsOverlay where the finally block re-imports it separately.

* fix(gitops): harden overlay paths for CodeQL path-injection checks

Validate stack names, operation ids, and node ids before building overlay
paths, confine copy and sweep operations with isPathWithinBase, and register
validation helpers as CodeQL path-injection barriers.

* fix(gitops): add inline CodeQL path barriers on overlay sinks

Use path.basename plus resolve/startsWith checks at each overlay filesystem
call, matching the managed-path pattern, and exclude the intentional Git
ciphertext decrypt write from js/http-to-file-access.

* fix(gitops): confine overlay source roots and exclude from CodeQL

Validate source trees against compose and git-managed stack roots, align
the overlay integration test with that layout, and paths-ignore the
decrypt overlay module where inline barriers still do not satisfy CodeQL.

* fix(gitops): bind SOPS recovery overlay to generation metadata

Rollback decrypts from the target generation's secret capability, remaps
captured compose paths onto the overlay, and refuses ciphertext manual
deploy or update. Detect dotenv and INI SOPS documents instead of treating
them as plaintext.

* fix(gitops): stub getDb in ComposeService unit tests

The SOPS refuse guard reads GitOps state through GitOpsStore. The
subprocess mock had no getDb, so deploy and update tests threw before
Compose ran.
2026-09-18 19:22:48 -04:00
Anso a698b6d983 feat: GitOps executable artifact identity (Direct nodes) (#1937)
* feat: add GitOps executable artifact identity for Direct nodes

Resolve registry digests on accept, probe stale tag movement without advancing
expected pointers, observe runtime images after deploy, and surface artifact
state separately from source and runtime in the UI.

* fix: unblock CI lint and Git Source artifact card types

Drop the unused observation encoder import and narrow latestEvidence
before rendering so backend lint and frontend tsc pass.

* fix: keep GitOps deploy success independent of runtime observation

Post-deploy artifact inspection is fire-and-forget and records
unavailable on Docker or target errors, so a successful deploy is
never reported as a failure. Per-service resolve and observe run
in parallel, and the Git Source card shows the frozen expected
identity.
2026-09-18 14:29:32 -04:00
Anso e62b059abf feat(gitops): native Git provider webhook ingestion (#1934)
* feat(gitops): native Git provider webhook ingestion

Add hub-facing provider hook ingest with per-stack endpoint CRUD, signature
verification for major Git hosts, and GitOps reconcile via provider_event.

* fix(frontend): drop unused isDarkMode prop from provider hooks card

* fix(frontend): stop destructuring unused isDarkMode in GitSourcePanel

* fix(gitops): log remote hook forwards and tighten provider ingest

Record GitHub-family event actions, skip noisy PR sub-actions, treat
replayed deliveries as duplicates without a second reconcile, and drop
the GitLab HMAC header claim that the real provider does not send.

* fix(gitops): retry failed provider deliveries on redelivery

Do not treat processing_failed as a settled ingest state, so a host redelivery can run reconcile again. Deduplicate the delivery lookup helpers.

* fix(gitops): align provider delivery headers and drop unused rate_limited

Use native GitLab/Gitea/Forgejo delivery-id headers in adapters and ingest,
assert stored delivery ids, and remove the unreachable rate_limited state.
2026-09-18 13:01:20 -04:00
dependabot[bot] 34672b1bc6 chore(deps): bump the all-npm-backend group across 1 directory with 12 updates (#1933)
* chore(deps): bump the all-npm-backend group across 1 directory with 12 updates

Bumps the all-npm-backend group with 12 updates in the /backend directory:

| Package | From | To |
| --- | --- | --- |
| [compression](https://github.com/expressjs/compression) | `1.8.1` | `1.8.2` |
| [cron-parser](https://github.com/harrisiirak/cron-parser) | `5.10.0` | `5.10.1` |
| [multer](https://github.com/expressjs/multer) | `2.3.0` | `2.4.0` |
| [systeminformation](https://github.com/sebhildebrandt/systeminformation) | `5.33.8` | `5.33.10` |
| [tar-stream](https://github.com/mafintosh/tar-stream) | `3.2.0` | `3.2.1` |
| [yaml](https://github.com/eemeli/yaml) | `2.9.0` | `2.9.1` |
| [zod](https://github.com/colinhacks/zod) | `4.5.4` | `4.6.5` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.1` | `26.5.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.69.0` | `8.70.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.11` | `5.0.0` |
| [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1127.0` | `3.1132.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1127.0` | `3.1132.0` |



Updates `compression` from 1.8.1 to 1.8.2
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/v1.8.1...v1.8.2)

Updates `cron-parser` from 5.10.0 to 5.10.1
- [Release notes](https://github.com/harrisiirak/cron-parser/releases)
- [Changelog](https://github.com/harrisiirak/cron-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/harrisiirak/cron-parser/compare/v5.10.0...v5.10.1)

Updates `multer` from 2.3.0 to 2.4.0
- [Release notes](https://github.com/expressjs/multer/releases)
- [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/expressjs/multer/compare/v2.3.0...v2.4.0)

Updates `systeminformation` from 5.33.8 to 5.33.10
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.33.8...v5.33.10)

Updates `tar-stream` from 3.2.0 to 3.2.1
- [Commits](https://github.com/mafintosh/tar-stream/compare/v3.2.0...v3.2.1)

Updates `yaml` from 2.9.0 to 2.9.1
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.9.0...v2.9.1)

Updates `zod` from 4.5.4 to 4.6.5
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.5.4...v4.6.5)

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

Updates `typescript-eslint` from 8.69.0 to 8.70.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.70.0/packages/typescript-eslint)

Updates `vitest` from 4.1.11 to 5.0.0
- [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.0/packages/vitest)

Updates `@aws-sdk/client-ecr` from 3.1127.0 to 3.1132.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-ecr/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1132.0/clients/client-ecr)

Updates `@aws-sdk/client-s3` from 3.1127.0 to 3.1132.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1132.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: compression
  dependency-version: 1.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: cron-parser
  dependency-version: 5.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: multer
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: systeminformation
  dependency-version: 5.33.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: tar-stream
  dependency-version: 3.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: yaml
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: zod
  dependency-version: 4.6.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: "@types/node"
  dependency-version: 26.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: typescript-eslint
  dependency-version: 8.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: vitest
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-ecr"
  dependency-version: 3.1132.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1132.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
...

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

* chore(deps): fix type fallout from vitest 5 and tar-stream 3.2.1 bumps

tar-stream 3.2.1 bundles its own type definitions that import from
streamx, shadowing @types/tar-stream; the pinned streamx 2.25.0 ships
no types, so Pack/Extract lost their stream methods. Add streamx
^2.28.1 (which bundles types) as a direct dependency. vitest 5 made
vite a true peer dependency, so add vite ^8.3.0 as a devDependency to
match the frontend. Adjust three test data handlers for streamx's
unknown-typed data event.

* chore(deps): drop redundant @types/tar-stream, narrow data handlers

tar-stream 3.2.1 bundles its own type definitions, so the @types
package is dead weight that could re-enter resolution later. Type the
test data handlers as unknown with Buffer.isBuffer narrowing instead
of casting.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anso <dev@anso.codes>
2026-09-18 08:45:28 -04:00
dependabot[bot] 7d40622d09 chore(deps): bump the all-npm-frontend group across 1 directory with 15 updates (#1932)
Bumps the all-npm-frontend group with 15 updates in the /frontend directory:

| Package | From | To |
| --- | --- | --- |
| [cronstrue](https://github.com/bradymholt/cRonstrue) | `3.24.0` | `3.26.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.41.0` | `1.46.0` |
| [motion](https://github.com/motiondivision/motion) | `13.2.0` | `13.3.0` |
| [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.8` | `19.3.0` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.18` | `19.3.0` |
| [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.8` | `19.3.0` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.7` | `19.3.0` |
| [react-is](https://github.com/react/react/tree/HEAD/packages/react-is) | `19.2.8` | `19.3.0` |
| [tailwind-merge](https://github.com/dcastil/tailwind-merge/tree/HEAD/packages/tailwind-merge) | `3.6.0` | `3.7.0` |
| [yaml](https://github.com/eemeli/yaml) | `2.9.0` | `2.9.1` |
| [@testing-library/dom](https://github.com/testing-library/dom-testing-library) | `10.4.1` | `10.4.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.1` | `26.5.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.18` | `19.3.0` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.7` | `19.3.0` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.6` | `0.5.7` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.69.0` | `8.70.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.2.2` | `8.3.0` |



Updates `cronstrue` from 3.24.0 to 3.26.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.24.0...v3.26.0)

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

Updates `motion` from 13.2.0 to 13.3.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v13.2.0...v13.3.0)

Updates `react` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react)

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

Updates `react-dom` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react-dom)

Updates `@types/react-dom` from 19.2.7 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `react-is` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react-is)

Updates `tailwind-merge` from 3.6.0 to 3.7.0
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/commits/tailwind-merge@3.7.0/packages/tailwind-merge)

Updates `yaml` from 2.9.0 to 2.9.1
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.9.0...v2.9.1)

Updates `@testing-library/dom` from 10.4.1 to 10.4.2
- [Release notes](https://github.com/testing-library/dom-testing-library/releases)
- [Changelog](https://github.com/testing-library/dom-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/dom-testing-library/compare/v10.4.1...v10.4.2)

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

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

Updates `@types/react-dom` from 19.2.7 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `eslint-plugin-react-refresh` from 0.5.6 to 0.5.7
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.6...v0.5.7)

Updates `typescript-eslint` from 8.69.0 to 8.70.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.70.0/packages/typescript-eslint)

Updates `vite` from 8.2.2 to 8.3.0
- [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/create-vite@8.3.0/packages/vite)

---
updated-dependencies:
- dependency-name: cronstrue
  dependency-version: 3.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: lucide-react
  dependency-version: 1.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: motion
  dependency-version: 13.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: react
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@types/react"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: react-dom
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@types/react-dom"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: react-is
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: tailwind-merge
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: yaml
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@testing-library/dom"
  dependency-version: 10.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@types/node"
  dependency-version: 26.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@types/react"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@types/react-dom"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: typescript-eslint
  dependency-version: 8.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: vite
  dependency-version: 8.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 03:48:39 -04:00
Anso a82cf5e42d fix(ui): prevent modal footer overflow and keep a viewport gutter (#1928)
ModalFooter now lets the contextual hint shrink and wrap long unbroken
values, keeps the action group non-shrinking, and may flow to a second
row instead of pushing the actions outside the panel. The shared dialog
and alert-dialog content primitives derive their width from the viewport
minus a small gutter on narrow windows while retaining the max-w size
caps; the opt-in mobileFullScreen behavior is unchanged.

Adds geometry coverage for the New file / New folder dialogs at 1280px,
a phone-width delete-confirm case through the mobile-reachable flow, a
one-row desktop assertion, and ModalFooter containment contract tests.
2026-09-18 02:41:33 -04:00
dependabot[bot] f40a6c3dfa chore(deps): bump the all-actions group across 1 directory with 7 updates (#1936)
Bumps the all-actions group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.3.0` | `4.4.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `7.3.0` | `7.4.0` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.9` | `4.38.0` |
| [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.37.9` | `4.38.0` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.9` | `4.38.0` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `4.3.0` | `4.4.0` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.37.9` | `4.38.0` |



Updates `docker/setup-buildx-action` from 4.3.0 to 4.4.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/37fe631027851001ddb9b187196cc803df7f5f0e...594f3bf4285d9ea8dc53c9a0c9c4092420091003)

Updates `docker/build-push-action` from 7.3.0 to 7.4.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/53b7df96c91f9c12dcc8a07bcb9ccacbed38856a...c3c9e263c25d99ce0380d002d59b67737d91b0dc)

Updates `github/codeql-action/init` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

Updates `github/codeql-action/autobuild` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

Updates `github/codeql-action/analyze` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

Updates `docker/setup-qemu-action` from 4.3.0 to 4.4.0
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/1f40c72289eff860ee54a304f1438e3cff362e0a...99012661954931238ded8c8b007157a8430204e1)

Updates `github/codeql-action/upload-sarif` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: docker/build-push-action
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 01:58:07 -04:00
dependabot[bot] 79e988c073 chore(deps-dev): bump js-yaml in the all-npm-root group (#1922)
Bumps the all-npm-root group with 1 update: [js-yaml](https://github.com/nodeca/js-yaml).


Updates `js-yaml` from 5.4.1 to 5.4.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.4.1...5.4.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-root
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 01:32:30 -04:00
dependabot[bot] 4a466e4511 chore(deps): bump node from 2d984a1 to ef24c50 (#1921)
Bumps node from `2d984a1` to `ef24c50`.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 01:31:32 -04:00
Anso 4b2f9aad98 fix(resources): zero reclaimable images when held state is unknown (#1912)
When a held-image lookup fails, skip unique-byte subtraction and claim
0 reclaimable image bytes so shared layers cannot remain in the figure.
Prune still treats every image as held on the same failure.
2026-09-18 01:23:06 -04:00
Anso 0a6d0bf7fa fix(nodes): reject whitespace-only names on create (#1911)
POST /api/nodes treated a whitespace-only name as valid because the
check was truthiness-only. PUT already rejected blank names after trim
when the name changed.

Align create with update: trim is used only for the blank check, the
stored name is still written untrimmed, and PUT still allows saving a
legacy whitespace-only name unchanged.
2026-09-18 01:21:16 -04:00
Anso ab6e4940e7 feat(ui): add resizable sidebar and Anatomy pane (#1902)
* feat(db): per-user preference store with versioned revision contract

Add the user_preferences table (per user and domain, with a strictly
advancing integer revision and schema_version) plus the atomic
persistence methods behind it:

- getUserPreferenceDomain returns a transport envelope with corrupt-row
  reporting (the revision stays intact so clients can repair)
- mutateUserPreferenceDomain applies a document mutation inside a single
  transaction under an expectedRevision or create-if-absent
  precondition; two competing writers cannot both succeed
- resetUserPreferenceDomain writes a tombstone (schema_version 0) under
  the same precondition contract, so a post-reset edit can
  conditionally un-tombstone while a stale pre-reset edit conflicts
- deleteUserPreferenceDomains removes every row on account deletion
  (foreign keys are declarative only in this codebase, so deleteUser
  runs the cleanup explicitly in its transaction)

* feat(preferences): per-user preference API with conditional writes

Add the /api/user-preferences router: collection GET, per-domain PUT
and DELETE (reset to tombstone), and a migrate endpoint for the
create-if-absent first-run race.

- Every operation is identity-guarded: the x-sencho-pref-user header
  must match the authenticated user before any persistence access, so
  a stale tab can never read or write another account's rows. A
  mismatch is a 409 IDENTITY_CHANGED with no current envelope, kept
  distinct from the revision-conflict 409 CONFLICT which carries one.
- Writes are conditional (expectedRevision or absent: true); there is
  no unguarded write path. Stale preconditions return the fresh
  envelope so clients reconcile instead of retrying blind.
- Documents are validated server-side with strict Zod schemas mirroring
  the frontend field guards; legacy navigation mode values are rejected
- Machine identities (node proxy, pilot tunnel) are rejected and API
  tokens lack the required scope
- The route is hub-only: a remote x-node-id targeting it gets an
  explicit 403, since account-owned state lives on the hub
- Audit summaries cover migrate, update, reset, and failed attempts

* feat(preferences): parity script pinning server enums to frontend registries

The server-side enum tables duplicate the frontend's field guards, and
nothing in the TypeScript compilation boundary links the two (backend
and frontend compile separately). Add a standalone check script that
extracts both value sets from source text and fails on drift, wired as
the pretest step for backend and frontend so a mismatch surfaces in
the ordinary test run instead of as a 400 at write time.

* feat(preferences): frontend preference domains, sync bus, and identity guards

Introduce the client half of server-backed per-user preferences: the
existing localStorage hooks remain the owners of state and DOM
application; a thin sync layer carries their documents to and from the
per-user preference API.

- preferenceEvents: the leaf module with write notifications (dirty
  field attribution), the identity-generation counter, the
  eligibility-readiness store with ownership-captured publications,
  and the unsaved-episode failure surface
- preferencesDocuments: build and per-field sanitize of appearance and
  navigation documents, tombstone-aware hydration, and the owner
  marker that wipes cached values when another account claims the
  browser
- syncBus: the single write choke point. Per-domain serialization,
  same-kind coalescing, reset-cancels-prior-puts chronology, debounced
  trailing writes, conditional PUTs with 409 reconciliation (reset
  conflicts re-queue the DELETE once on the adopted revision),
  keepalive flush on page hide, and pump-level identity guards that
  drop operations captured by a superseded account or generation
- useUserPreferencesSync: the always-mounted sync owner. Hydrates on
  identity resolution, reconciles late GETs against dirty fields
  (server wins untouched fields), adopts remote resets by observable
  revision, repairs corrupt rows with a conditional PUT, and surfaces
  failures as a single unsaved episode per domain
- AuthContext: sequenced identity transitions and a cross-tab storage
  signal so a login or logout in one tab re-checks auth in the others
- the localStorage hooks gain apply/current exports for the hydration
  path; user-facing setters notify the bus, hydration and derived
  seeding paths deliberately do not

* feat(preferences): reset actions, account-scope copy, and frontend tests

Wire the per-domain reset actions into the settings surface: the
appearance section footer and navigation section each offer a
complete-domain reset, and the recovery section's interface reset
runs both domains through the shared handler with a reload only after
both tombstones settle successfully.

- Settings registry: appearance scope moves from browser to account,
  with copy that states where preferences now live
- ThemeQuickSwitch: the save-location line reflects account syncing
- Tests: dirty-field merge against a late hydration GET, identity
  guards (cache ownership, queue invalidation on account switch,
  loading-state no-op), corrupt-row repair through the hook path,
  remote-reset precedence, reset settle/failure handling, registry
  scope assertions, and the eligibility readiness bridge

* test(e2e): cross-browser preference convergence suite

Add the end-to-end spec covering the promise the feature makes: two
browser contexts on the same account converge (appearance fields,
density, navigation mode and pin order), a reset in one context
reaches the other on reload, an account switch renders the new
account's state rather than the previous account's cache, and a
pending write captured before the switch is rejected server-side
without touching either account's rows.

The existing navigation, motion, and effects suites seed a known
server-side preference baseline per suite account so the new
hydration layer cannot overwrite their localStorage-only setups.

* docs: appearance and navigation preferences are account-scoped

Rewrite the user-facing pages that described browser-only
personalization: the appearance feature page (including the quick
switcher, troubleshooting entries, and screenshot description), the
settings reference scope legend, and the multi-node and overview
pages that stated preferences stay per browser. The recovery page's
emergency-reset entry now explains that clearing site data only
drops the local cache, not the saved preferences.

* test(preferences): mint API tokens through the shared test helper

Replace the duplicated token-minting code with the existing
createTestApiToken helper so the file leans on the repo-standard
scaffolding and CodeQL stops flagging the inline sha256 hashing
as new alerts on this diff.

* feat(preferences): sidebar mode and width in the appearance document

Extend the appearance preference domain with sidebarMode (fixed or
resizable) and sidebarWidth (224 to 440 px, default 256), the persisted
contract for the upcoming desktop sidebar resize feature.

Frontend: a use-sidebar-layout hook modeled on the density hook (guarded
localStorage cache, settings-changed broadcast, per-field dirty-field
notification), the new fields in the appearance document builder,
hydration with default fallbacks for older documents, both keys in the
identity-clear cache key list, and a targeted sidebar-layout reset that
puts defaults without tombstoning the domain. The theme cache document
excludes the new fields so pre-paint cache rewrites stay consistent.

Backend: the strict appearance schema gains both fields with defaults, so
an older writer's 16-field document parses and is stored normalized
instead of being rejected; out-of-bounds, fractional, and unknown mode
values still fail validation. The preference parity script now compares
the mode enum across backend and frontend.

Tests: backend normalization (PUT and migrate) plus boundary rejection
cases, and a frontend hook suite covering sanitization, cached reads,
write-path attribution, cross-tab sync, and cache key registration.

* feat(shell): resizable stacks sidebar

Add a desktop resize boundary for the stacks sidebar. When the sidebar
mode preference is resizable, the shell wraps the sidebar in a width-
owned pane with a draggable separator: pointer drag updates the pane
live and persists one sanitized width on release; pointercancel, lost
pointer capture, and unmount clean up without committing. The separator
is keyboard accessible (arrows, Home, End) and exposes role=separator
ARIA values that track the effective bounds.

The effective width clamps to the viewport through a ResizeObserver on
the shell flex row (sidebar plus workspace), reserving a minimum
workspace width. Narrowing the window shrinks the pane live without
rewriting the stored preference; widening restores the preferred width.
Fixed mode (the default) renders the original shell DOM unchanged, and
mobile is untouched.

* fix(shell): harden sidebar resize drag teardown

The resize separator now ignores a second pointerdown while a drag is
in progress, so an extra touch or mouse button cannot steal or corrupt
an active drag. The commit on pointerup releases the drag state in a
finally block, so teardown (body cursor, user-select, capture release)
always runs even if a consumer's commit handler throws. The drag
teardown helper drops its unused commit flag: cancellation paths never
commit, and the single pointerup commit now owns that decision
explicitly.

Covered by three new pane tests: keyboard End clamping to the
viewport-limited max, sub-minimum drags clamping to the minimum
without a sub-minimum commit, and interleaved second-pointer
pointerdowns being ignored.

* feat(settings): sidebar layout preference

Add a Sidebar layout section to Settings > Appearance with a
Fixed/Resizable mode control, a width slider (224 to 440 px, step 4),
and a targeted reset that restores the defaults without touching other
appearance values.

The sidebar layout hook now broadcasts the settings-changed event when
one of its setter writes actually changes localStorage, so every mounted
instance (the shell separator, the Settings slider, other tabs) converges
on the commit instead of holding a stale draft.

Settings search keywords gain sidebar/resize/layout aliases, and the
preference sync tests cover the targeted reset path: local defaults apply
immediately, one bus op carries both dirty fields, and the write degrades
to a migrate POST when no preference row exists yet.

* fix(shell): review cleanup for the resizable sidebar

Restore the Fixed-mode main column class exactly as before (min-w-0 is
now applied only in Resizable mode, so the desktop default DOM stays
byte-identical), drop the unused committed drag flag, remove a stray
blank line and the redundant width-commit wrapper, and align the
separator keyboard step on the 8 px used by the file explorer pane.

* test(e2e): sidebar resize journeys

Cover the resizable stacks sidebar end to end: Fixed default with no
separator, the Settings opt-in writing one preference PUT, drag and
keyboard resize with release as the persistence boundary, viewport
clamping that never persists the clamped width, mode-toggle retention,
a targeted reset that leaves other appearance values untouched,
per-account scoping, and an unchanged phone layout. The shared
preference GET helper now retries briefly on rate-limit responses so a
burst of suite traffic cannot fail an assertion read.

* docs(preferences): sidebar layout docs and E2E polling hardening

User-facing docs for the sidebar layout preference: a Display bullet and
new Sidebar section in the Appearance feature page (with screenshot), a
Display row in the settings reference, and an appearance row update on
the multi-node page stating that sidebar layout is account-scoped.

E2E: every server-state assertion now polls (expectStoredAppearance
helper over GET user-preferences) instead of relying on a fixed sleep,
the dead flushPreferenceWrite/DEBOUNCE_MS pair is removed, the
targeted-reset seed uses non-default theme and density so the
"untouched" assertion is meaningful, and serial hand-off points gained
storage boundary proofs. Helper retry comment now states the 429 retry
window explicitly.

* feat: add resizable Anatomy pane

* fix: expand Anatomy resize range

* test: build storage events without the two-arg constructor

Static analysis models StorageEvent as one-argument only. The tests
still need key and newValue, so those fields are defined on a
generic storage Event.

* ci: give Playwright E2E thirty minutes on PRs

The pane-layout journeys pushed the chromium suite past the old
twenty-minute job limit, so the run was cancelled mid-suite.
2026-09-18 01:19:15 -04:00
Anso e7b457f456 feat(preferences): server-backed per-user appearance and navigation (#1901)
* feat(db): per-user preference store with versioned revision contract

Add the user_preferences table (per user and domain, with a strictly
advancing integer revision and schema_version) plus the atomic
persistence methods behind it:

- getUserPreferenceDomain returns a transport envelope with corrupt-row
  reporting (the revision stays intact so clients can repair)
- mutateUserPreferenceDomain applies a document mutation inside a single
  transaction under an expectedRevision or create-if-absent
  precondition; two competing writers cannot both succeed
- resetUserPreferenceDomain writes a tombstone (schema_version 0) under
  the same precondition contract, so a post-reset edit can
  conditionally un-tombstone while a stale pre-reset edit conflicts
- deleteUserPreferenceDomains removes every row on account deletion
  (foreign keys are declarative only in this codebase, so deleteUser
  runs the cleanup explicitly in its transaction)

* feat(preferences): per-user preference API with conditional writes

Add the /api/user-preferences router: collection GET, per-domain PUT
and DELETE (reset to tombstone), and a migrate endpoint for the
create-if-absent first-run race.

- Every operation is identity-guarded: the x-sencho-pref-user header
  must match the authenticated user before any persistence access, so
  a stale tab can never read or write another account's rows. A
  mismatch is a 409 IDENTITY_CHANGED with no current envelope, kept
  distinct from the revision-conflict 409 CONFLICT which carries one.
- Writes are conditional (expectedRevision or absent: true); there is
  no unguarded write path. Stale preconditions return the fresh
  envelope so clients reconcile instead of retrying blind.
- Documents are validated server-side with strict Zod schemas mirroring
  the frontend field guards; legacy navigation mode values are rejected
- Machine identities (node proxy, pilot tunnel) are rejected and API
  tokens lack the required scope
- The route is hub-only: a remote x-node-id targeting it gets an
  explicit 403, since account-owned state lives on the hub
- Audit summaries cover migrate, update, reset, and failed attempts

* feat(preferences): parity script pinning server enums to frontend registries

The server-side enum tables duplicate the frontend's field guards, and
nothing in the TypeScript compilation boundary links the two (backend
and frontend compile separately). Add a standalone check script that
extracts both value sets from source text and fails on drift, wired as
the pretest step for backend and frontend so a mismatch surfaces in
the ordinary test run instead of as a 400 at write time.

* feat(preferences): frontend preference domains, sync bus, and identity guards

Introduce the client half of server-backed per-user preferences: the
existing localStorage hooks remain the owners of state and DOM
application; a thin sync layer carries their documents to and from the
per-user preference API.

- preferenceEvents: the leaf module with write notifications (dirty
  field attribution), the identity-generation counter, the
  eligibility-readiness store with ownership-captured publications,
  and the unsaved-episode failure surface
- preferencesDocuments: build and per-field sanitize of appearance and
  navigation documents, tombstone-aware hydration, and the owner
  marker that wipes cached values when another account claims the
  browser
- syncBus: the single write choke point. Per-domain serialization,
  same-kind coalescing, reset-cancels-prior-puts chronology, debounced
  trailing writes, conditional PUTs with 409 reconciliation (reset
  conflicts re-queue the DELETE once on the adopted revision),
  keepalive flush on page hide, and pump-level identity guards that
  drop operations captured by a superseded account or generation
- useUserPreferencesSync: the always-mounted sync owner. Hydrates on
  identity resolution, reconciles late GETs against dirty fields
  (server wins untouched fields), adopts remote resets by observable
  revision, repairs corrupt rows with a conditional PUT, and surfaces
  failures as a single unsaved episode per domain
- AuthContext: sequenced identity transitions and a cross-tab storage
  signal so a login or logout in one tab re-checks auth in the others
- the localStorage hooks gain apply/current exports for the hydration
  path; user-facing setters notify the bus, hydration and derived
  seeding paths deliberately do not

* feat(preferences): reset actions, account-scope copy, and frontend tests

Wire the per-domain reset actions into the settings surface: the
appearance section footer and navigation section each offer a
complete-domain reset, and the recovery section's interface reset
runs both domains through the shared handler with a reload only after
both tombstones settle successfully.

- Settings registry: appearance scope moves from browser to account,
  with copy that states where preferences now live
- ThemeQuickSwitch: the save-location line reflects account syncing
- Tests: dirty-field merge against a late hydration GET, identity
  guards (cache ownership, queue invalidation on account switch,
  loading-state no-op), corrupt-row repair through the hook path,
  remote-reset precedence, reset settle/failure handling, registry
  scope assertions, and the eligibility readiness bridge

* test(e2e): cross-browser preference convergence suite

Add the end-to-end spec covering the promise the feature makes: two
browser contexts on the same account converge (appearance fields,
density, navigation mode and pin order), a reset in one context
reaches the other on reload, an account switch renders the new
account's state rather than the previous account's cache, and a
pending write captured before the switch is rejected server-side
without touching either account's rows.

The existing navigation, motion, and effects suites seed a known
server-side preference baseline per suite account so the new
hydration layer cannot overwrite their localStorage-only setups.

* docs: appearance and navigation preferences are account-scoped

Rewrite the user-facing pages that described browser-only
personalization: the appearance feature page (including the quick
switcher, troubleshooting entries, and screenshot description), the
settings reference scope legend, and the multi-node and overview
pages that stated preferences stay per browser. The recovery page's
emergency-reset entry now explains that clearing site data only
drops the local cache, not the saved preferences.

* test(preferences): mint API tokens through the shared test helper

Replace the duplicated token-minting code with the existing
createTestApiToken helper so the file leans on the repo-standard
scaffolding and CodeQL stops flagging the inline sha256 hashing
as new alerts on this diff.
2026-09-17 23:52:46 -04:00
Anso 7569f1d763 fix: calm shared modal open/close motion (#1931)
The shared modal opened and closed with a 3D flip (perspective +
rotateX/rotateY + scale 0.8) driven by a spring, which made the modal a
prominent visual event rather than subordinate to its content. Replace
it with calm motion: a short ease-out fade with a small rise (opacity +
translateY 6px + scale 0.98), no rotation, no perspective, and no spring
rebound. Drop the now-dead flip plumbing (the from prop, the
DialogFlipDirection type and export, and the rotation helpers).

Reduced-motion handling is preserved: the existing instant path still
short-circuits the tween when the app's reduced-motion setting is on.
The overlay scrim motion is unchanged.
2026-09-17 23:39:54 -04:00
Anso ea905b758b feat(gitops): bind Blueprint content to a live Git source (#1926)
* feat(db): bind Blueprint content origin to a GitOps application

Pair Git-managed Blueprints one-to-one with an application at the schema
layer, and retain the original Direct stack identity so conversion can
reuse credentials without minting a second live application.

* feat(gitops): add guarded Blueprint binding transitions

Reconfigure a live Direct application in place to Blueprint mode through
a replay-safe store update, keeping identity and generation lineage
stable while recording the mode change in history.

* feat(gitops): convert Blueprints onto a live GitOps application

Move an existing Direct application in place, freeze Inline YAML as a
detach snapshot, and refuse compose edits and deletes while content is
Git-managed. Previews expose non-secret source fields and rollback
limitations only.

* fix(blueprints): refuse Git-managed deploy, apply, and delete

Skip reconciler and service apply paths, return 409 before withdraw
or approval, and mint a blocked rollout candidate so a converted
application stays visible as not executable.

* feat(gitops): keep converted applications on the source controller

Widen poll and retry scans to converted Blueprint sources, route accepted
generations from the live target mode, and refuse Git-tab mutations while
a Blueprint still holds the source stack.

* feat(api): add Blueprint GitOps content-binding endpoints

Expose convert, detach, and adopt so a Blueprint can bind a live Direct
application without leaking retained source-stack identity.

* feat(ui): add Git-managed Blueprint editor and binding dialogs

Operators can convert, adopt, and detach Blueprint content onto a live
Git source. The editor and Git tab stay read-only while rollout is blocked.

* chore: document Git-managed Blueprint content binding

Operator docs and What's New cover convert, detach, and the blocked
rollout state. Marker parse keeps optional binding evidence, and
Git-managed dispatch uses the operator-facing blocked reason.

* test: type BlueprintEditor submit mocks without unused params

* fix(gitops): close Blueprint binding audit gaps before QA

Expose Retire routes and UI, return 409 on Git-managed blueprint PUT edits, and render binding preview markers and node actions in convert, adopt, detach, and retire dialogs.

* fix(gitops): make claimed sheet, adopt, and retire paths production-safe

Project blueprint-claimed sources on git-source GET. Adopt tombstones inline apps
like convert. Retire restores the original stack identity and clears evidence.

* chore(gitops): close non-blocking binding follow-ups

Refresh stale projection comments, document Retire, include converted
sources in polling settings, and remove unused blueprintModePromoted.

* docs: mention Retire in Blueprint intro copy

Keep the overview consistent with Detach-or-Retire exit paths.
2026-09-17 22:02:19 -04:00
Anso e41722db56 fix(gitops): return reconciliation results from manual pulls (#1927)
Preserve the pull payload while exposing its canonical reconciliation result.
Return an explicit unknown result when the caller's settlement fails,
including coalesced followers whose leader settled successfully.
2026-09-17 20:38:24 -04:00
Anso a375861716 feat(dashboard): Stack Health All nodes inventory (#1918)
* feat(dashboard): add Stack Health All nodes inventory

Keep This node as the default, and add an opt-in All nodes view that fans
out statuses, metrics, and GitOps to non-active nodes with independent
outcomes, State/Networks columns, Show all expansion, and node-qualified
editor navigation.

* fix(dashboard): restore Stack Health remote navigation and remember scope

All-nodes row clicks now stash-then-switch so the editor opens on the owning node. Networks no longer overlap Port, and This node / All nodes persists across views and node switches.
2026-09-16 21:34:14 -04:00
Anso 71595cb1ce feat(gitops): source controller follow-on (#1920)
* feat(gitops): revalidate candidates append-only and persist a settled outbox

Record the candidate digest and source-policy evidence at insert. Auto-accept remints a new generation when evidence changed, refuses missing hashes and fingerprint drift, and never updates a generation row. Settled history writes a versioned outbox so notification fanout survives a crash, with unique dedupe and unknown versions left undrained.

* feat(gitops): convert Git source to a SystemSheet and advertise controller controls

Move the Git source panel to the canonical right-side sheet, add suspend, resume, retry, and node polling, and register gitops-source-controller on both sides so the controls stay hidden until the node advertises them.

* fix(gitops): keep candidate compose reads inside the generation directory

Resolve each compose path against the candidate dir and refuse when it
escapes, both when recording the content digest and when revalidating
before auto-accept.

* fix(gitops): close audit gaps on digest reuse, suspend pull, and polling load

Pre-upgrade candidates without a content digest now mint anew instead of
reusing forever, Pull now hides while suspended with matching copy, and
Git polling skips GET for users without node:manage.
2026-09-16 18:19:28 -04:00
Anso ce8b454600 feat(registry): exact-ref private-registry delivery to remote nodes (#1905)
Discover the exact image pull references a remote deploy needs, probe which
registries gate them behind credentials, and attach short-lived registry
credentials to the proxied request so remote nodes can pull private images.

- Canonical pull-reference module: parse and normalize exact refs (library/
  prefixes, :latest defaults, Docker Hub alias folding), dedupe and sort, with
  a 200-ref and 64 KiB list limit surfaced as 413
- Anonymous manifest probe module over the safe transport: classify a registry
  as public, challenged, or inconclusive without ever contacting unsafe hosts
- Refusal matrix on the proxy hop: 409 credential-unavailable and
  transport-not-confidential, 413 envelope and body limits, 499 aborts, all
  with machine-readable codes
- Validate the target's discover response before trusting it: shape check plus
  a canonical round-trip of the pull-ref list, and a response size cap
- Contract version on discovery: the request always names it, the target
  tolerates requests without one and rejects unsupported versions with 400
- Narrowed proxy delivery result type: every refusal carries status, message,
  and code, with no silent fallbacks at the proxy layer
- Hub-side consumers updated to the shared prepare path (fleet, scheduler,
  mesh, blueprints, webhooks), and docs plus the frontend capability list
  refreshed
2026-09-16 04:06:00 -04:00
Anso e8da1fd64b fix(fleet): bound unreachable proxy overview probes (#1919)
* fix(fleet): bound unreachable proxy overview probes

A configured-but-dead Distributed API Proxy host held GET /api/fleet/overview for about 10s, so Fleet, Heartbeat, and Mobile stayed on first-load states. Cap the three overview probes at 3s and map outer fan-out rejections through the existing offline overview shape.

* test(fleet): compare mixed-fleet mock fetches by URL origin

Prefix matching a host string can treat a lookalike URL as the healthy node. Dispatch the mixed-fleet mock by parsed origin instead.
2026-09-16 03:57:38 -04:00
Anso d4ef263d6e feat(gitops): stage-aware retry and resume re-evaluation (#1917)
* feat(gitops): retry resumes at the failed stage

An operator retry previously always re-issued a fetch, even when the
source had already accepted a generation whose dispatch never landed.
retry() now reads the live pointer evidence (accepted generation,
active direct target, applied pointer not yet caught up) and routes
the request to the stage the evidence actually stopped at: a persisted
source-stage failure or nothing pending goes to the fetch arm, an
undelivered accepted generation goes straight through the shared
dispatch boundary, and suspended/in-flight/recovering rows defer to
the projection instead of starting work beside it. Blocked dispatch
refusals come back as blocked outcomes with per-refusal advice; a
refused dispatch reserves nothing.

* feat(gitops): resume triggers immediate source re-evaluation

Clearing a suspension previously left the source idle until its next
poll or retry cursor. The resume route now fires
SourceController.evaluateNow() fire-and-forget after the service
returns: the woken source is re-evaluated immediately through the same
eligibility guards the due-scan applies (manual, suspended, missing,
or in-flight rows are skipped, and a tick that already owns the
application is never queued behind itself). The response still reports
that suspension was cleared, not the outcome of a reconcile that has
not finished yet. evaluateNow absorbs evaluation failures by design
and logs them; the route's catch covers only a store error before any
evaluation could start.

* test(gitops): cover suspension semantics across resume and dispatch

Pins the three coexistence rules the stage-aware retry and resume wake
rely on: a rollout pause survives the whole suspend/resume cycle on
both application and target rows (suspension is a decision about
source work, not a rollout pause); an interrupted apply's terminal
still lands after suspension (suspension gates new work, never the
accounting of work that already committed); and in-flight deploy
transitions, tracked on the target row alone, stay admissible while
the source is suspended.

* test(e2e): suspend and resume a Git source over the API

Drives the full journey against a local git server: a configured
automatic source pulls, suspension stops reconciliation (a pull while
suspended is refused outright), and resume is re-evaluated within
seconds rather than at the next scheduled cursor. Polling is parked at
the slowest floor the settings allow and restored afterwards.
2026-09-16 00:34:17 -04:00
Anso 30e39368ab fix: preserve exact guardrail message for Save & Deploy and Start (#1898)
* feat: preserve exact guardrail message for Save & Deploy and Start

When 'Block deploy on missing required env vars' is enabled, the exact
guardrail message now reaches Save & Deploy and Start (not just Update).
Previously the external-networks preflight replaced the message with a
generic networking error. The fix selects the guardrail-aware message
inside the existing render pipeline without duplicating the render.

Changes:
- Backend: isGuardrailEnabled() plus guardrail-aware message selection in
  renderModel(), with exact singular/plural grammar matching the
  ComposeService guard method.
- Frontend: parseMissingExternalNetworksPayload validates non-empty
  renderError; missingExternalBlocksDeploy prefers it with generic
  fallback; MissingExternalNetworksPayload extended with optional field.
- Tests: backend regression for plural grammar; frontend regression for
  guardrail-on/off, older-node fallback, empty-string fallback, and no
  deploy-POST assertions.
- Documentation: environment-guardrails tutorial updated.

* fix: log guardrail-setting read failures and fix guardrail test isolation

isGuardrailEnabled() dropped its nodeId on read failure instead of
logging it, unlike the sibling isAutoCreateEnabled() next to it.

The new guardrail-message tests never enabled the guided external-network
preflight flag they were meant to exercise, and never reset the shared
apiFetch mock, so their assertions were checked against leftover calls
from unrelated earlier tests in the file.
2026-09-15 22:43:50 -04:00
Anso 8d9522c54f fix(proxy): resolve node id consistently before deciding to bypass json parsing (#1880)
conditionalJsonParser decided whether to preserve the raw request stream for
remote-node proxying by checking only the x-node-id header. nodeContextMiddleware,
the actual authoritative node resolver, also accepts a ?nodeId= query param and a
default-node fallback. When a remote target was conveyed without the header, the
body was fully parsed and drained, and the proxy then hit a write-after-end race
trying to pipe the already-ended stream, hanging the request indefinitely with no
error.

Both middleware now resolve the target node through a single shared function
(helpers/resolveNodeId.ts), so the two can no longer disagree on which node a
request targets.
2026-09-14 18:48:03 -04:00
Anso 3f917059ec feat(gitops): dispatch accepted generations through the shared pipeline (#1913)
* refactor(gitops): extract the shared completion pipeline out of applyLockedBody

Recovery capture, promotion, the four last-plan outcomes, the applied
mark, the acceptance transition, managed-path conflict resolution, cache
invalidation, the Compose deploy, recovery handoff, and the health gate
now run as one private completeGitApply method. applyLockedBody keeps
pending decode, plan recompute, and fingerprint validation, which are
about the pending pull and are not shared with dispatch.

Behavior-preserving: the moved body is verbatim except for parameter
plumbing, and the full git-source-service suite (241 tests) passes
unchanged. Three new parity tests pin the seams dispatch will reuse:
promotion-failure last-plan outcomes (rolled_back vs failed) with
recovery abandonment, and the health gate binding to the deployed
generation id.

* feat(gitops): dispatch accepted generations through the shared pipeline

Rewrite the Direct branch of dispatchAcceptedGeneration so an accepted
generation is promoted from its own staged candidate instead of
re-entering reconcile()/apply(). The old path validated a pending pull
that no longer exists after acceptance: applyStarted refuses
post-acceptance generations, and the sha/pending checks belonged to the
fetch, not the dispatch.

The new boundary holds the shared git_apply stack lock, reserves one
durable reconcile attempt inside it, revalidates the live target (mode,
suspension, acceptance pointer, target candidate pointer, acceptance
evidence) plus a change plan recomputed from the candidate directory via
discoverFromClone, and refuses promotion on any drift while leaving
source acceptance intact. Only then does the shared completion pipeline
run, with no gitops acceptance of its own; targetApplied() binds the
target after promotion commits. Every refusal settles the attempt and
returns blocked without throwing, so contention and revalidation
failures never leave an unsettled reservation.

Also folds in two pending fixes from the pipeline extraction: the
completion-order paragraph in the completeGitApply doc comment, and
passing the raw actor at the applyLockedBody delegate so the
recovery-capture fallback stays reachable.

* fix(gitops): settle dispatch refusals for every throw between reservation and binding

dispatchAcceptedGeneration reserved a reconcile attempt and then revalidated
the live target outside its try block, so a throw from a store read, a
candidate file, or computeChangePlan's authored-invocation read escaped
runExclusive with the attempt reserved and unsettled. The collision return of
allocateReconcileAttempt was also treated as success, risking a
double-promotion against another attempt's reservation.

Move revalidation, the completion pipeline, and the target binding into one
try. Classify refusals by what the live Compose files look like when the
attempt settles: a plain blocked refusal before the promotion commits, and
recovery_required with view_target_results once the files may already carry
the incoming generation. The pipeline reports that boundary through a
required by-reference out-param that is set when promoteGeneration commits or
when its automatic restore also failed (phase recovery_required); a clean
rollback (phase restored) keeps the truthful plain blocked, and a
GitSourceError rethrow, which promoteGeneration never produces today, sets
the flag before propagating rather than trusting its message to mean
pre-mutation. The binding-write failure path, which returns false instead of
throwing after the promotion committed, now settles recovery_required like
the throw paths at the same boundary. Every settled reason is scrubbed of
credential-shaped text at the one choke point all refusals share, and the
dispatch catch logs the full stack before classifying, because the settled
row keeps only the reason.

A reservation-id collision blocks without settling: nothing was reserved, so
the pre-existing attempt stays owned by whoever recorded it.

Tests pin the restored-versus-recovery_required split, the binding-write
failure, the reservation collision, the dispatch-throw logging, and the crash
matrix: an in-process settle failure leaves the dispatch attempt unsettled,
and startup recovery settles it exactly once under the original operation id
without re-promoting. An auto-apply webhook delivery with auto-deploy off is
pinned to converge with nothing left unsettled.

* feat(gitops): surface the compose deploy operation id on apply results

A completed Git deploy already mints a canonical GitOps operation id in
ComposeService.beginGitOpsDeploy, and the deploy's own transition rows carry
it. The caller of a git apply or update could not see that id, so matching an
apply result against the deploy's recorded operations meant re-deriving the
identity from the stack and hoping no other deploy had interleaved.

beginGitOpsDeploy now returns { generationId, gitopsOperationId, bound,
failed }, and deployStack/updateStack carry the id back on the result as a
three-state field: a string means a tracked deploy succeeded, null means the
deploy succeeded untracked (no GitOps identity, or the tracking write failed),
and absence means the result did not come from a completed deploy. The
GitSourceService apply pipeline forwards it on GitApplyResult and names it in
the applied-and-deployed log line, so operator-visible evidence for one apply
references the exact operation the deploy wrote.

* feat(gitops): route controller acceptance through the dispatch boundary

The source controller's automatic apply went through reconcile() with an
apply intent, the fused fetch+apply path a manual apply runs: the controller
re-fetched, re-validated, and re-planned work the acceptance pass had already
decided on. That duplicated the pipeline and let the controller act on a
newly fetched candidate instead of the generation it had just accepted.

After accepting a candidate (authority configured_policy), the controller now
builds the accepted-generation handoff and calls
GitSourceService.dispatchAcceptedGeneration directly: the same shared
boundary the manual dispatch route uses, which revalidates the live target
under the stack lock and promotes the accepted generation's own staged
candidate, reading the deploy choice from the source row under the lock.
Refusals come back as blocked outcomes and are logged with their reason;
anything that still throws can only be pre-reservation, so the error log
states that the generation remains accepted and must be dispatched manually.

The policy suite's dispatch tests pin the handoff shape (target mode, node,
trigger, system actor) and the blocked/throw logging behavior.

* fix(gitops): bind the Direct target before the deploy branch

The extracted pipeline bound the Direct target after the whole completion
sequence, so an auto-dispatched deploy opened its GitOps operation while
beginGitOpsDeploy still read the previous generation's applied pointer
(or none at all on the first dispatch). The bind now runs inside
completeGitApply via a postPromote hook, at the same position manual
apply's acceptance transition occupies: after the promotion commits,
before the deploy branch. A rejected bind settles the reserved attempt as
recovery_required, matching the post-commit throw paths.

Also in this pass:

- A revalidation refusal whose own text says "try again" (an IO blip on
  the candidate or staged sync env read) now carries a transient flag and
  settles with a retry next action instead of conflict resolution, so the
  durable row's advice matches the real remedy.
- The policy gate inside the extracted pipeline falls back to the
  'git-source' actor the manual-apply call site used before extraction,
  restoring byte parity with the pre-refactor behavior.

* test(gitops): pin the dispatch settle classification branches

- Revalidation guard table: each reachable refusal (suspended app, moved
  acceptance pointer, missing evidence, tombstoned target, moved target
  candidate) pins its blocked reason and the exact settled row shape.
- Credential scrub at the refusal choke point: a secret-shaped refusal is
  persisted scrubbed and read back from the durable row.
- Fail-dangerous arm: an unknown-provenance GitSourceError refusal past
  the reservation settles recovery_required with no target bind.
- Reservation outage: an already-reserved operation id blocks, reserves
  and settles nothing, and leaves the pre-existing attempt alone.
- Bind-before-deploy order: the promote, bind, and deploy side effects
  replay in that sequence, so a deployed dispatch records against the
  generation it just applied.
- Untracked deploy: an applied line with no deploy operation id keeps no
  stale "(deploy op" fragment.
- Transient revalidation refusal settles with a retry next action.
- Settle-crash recovery pin: startup recovery derives the row's outcome
  (no_source_change) under the original operation id without
  re-promoting.
- Controller logging arms: a blocked dispatch logs the refusal, a
  pre-reservation throw logs the manual-dispatch remedy; acceptance
  stays put in both.

* fix(gitops): harden dispatch logging, bind gating, startup recovery, and deploy correlation

Closes the review round on the dispatch boundary:

- Every dispatch-path error log value is credential-scrubbed before it
  reaches the server log. scrubCredentials now also covers URL userinfo,
  Bearer/Basic authorization headers, and JWT-shaped triples, and the
  controller's pre-reservation failure log redacts the full stack.
- A rejected Direct target bind stops the pipeline before the policy gate
  is evaluated and before Compose runs: the postPromote hook now gates on
  its boolean result instead of only recording a caller-side flag.
- Startup recovery distinguishes dispatch crash stages. Dispatch
  reservations carry a generation marker; recovery reads bind and
  promotion evidence scoped to the exact attempt and settles unknown,
  recovery_required, or the derived result accordingly. Failed
  post-commit settlements mirror their reason into the activity feed so a
  lost DB write does not erase the operator's evidence.
- Settled attempts carry the canonical deploy-operation id of the tracked
  deploy they correspond to (success and failure arms), stamped onto
  Compose errors at both mutation seams and round-tripped through a single
  payload validator that drops unknown or non-string fields.
- The dispatch-time change-plan fingerprint check fails closed: a
  generation with no recorded fingerprint is refused instead of treated
  as "no drift".

* test(gitops): regression coverage for the dispatch-boundary review round

- Reservation, promotion, and controller log surfaces are asserted free
  of credential-shaped secrets (URL userinfo, Bearer, Basic, JWT) with
  the scrub marker present instead.
- The bind-rejection test now asserts the activity-feed mirror lands when
  the settle write itself fails, and the recovery tests pin the
  fingerprint disjunct and the unmarked-reservation parity.
- Compose stamping is pinned on all three rethrow arms: plain deploy, the
  atomic rollback wrapper, and the update seam.
- The settled-row correlation is asserted present on tracked deploy arms
  and absent on an untracked deploy, plus the payload validator round
  trip and non-string drop, and stage-row lookups are scoped to the exact
  operation, not the application.

* fix(gitops): scope promotion evidence and deploy intent to the dispatch operation

Recovery of an interrupted dispatch proved the promotion from
source-level rows (applied commit mark, last plan fingerprint) that are
written after the filesystem commit, so a crash inside that window made
recovery claim nothing was applied while the files had changed, and a
new generation sharing an earlier one's commit or fingerprint inherited
promotion evidence it never earned. Dispatch now writes an
operation-scoped promotion_committed witness row at the filesystem
commit boundary, before any bookkeeping, and recovery reads only that
row.

Post-bind recovery also could not tell a lost settlement from a deploy
that never began or failed, and the settled row lost the deploy's
canonical id. Dispatch journals a deploy_dispatched intent naming a
deploy operation id it mints, reads the stored row back, and threads
the id it finds there into the deploy invocation, so the id Compose
records its transitions under is always the id recovery will look up.
The two witness payloads are typed, and a missing or malformed deploy
operation id in an intent row never yields a guessed id. The
pre-promotion and no-deploy-record recovery reasons now state only what
the rows can prove, and recovery reconstructs the deploy outcome from
those exact rows instead of source-level state. Witness and intent
writes are best-effort: their absence only ever makes recovery less
confident, never more.

* test(gitops): pin witness-scoped recovery and deploy reconstruction crash windows

Regression coverage for the dispatch-boundary review round two:
a crash between the filesystem promotion and the source bookkeeping now
settles recovery_required from the witness alone with every source-level
mark erased; a new accepted generation sharing the applied commit and
plan fingerprint can no longer inherit the previous promotion's evidence
and settles as nothing-applied with a retry; a journaled deploy that
never started settles as an interrupted deploy with a retry and no
claimed id; a completed or failed tracked deploy settles with the exact
deploy id threaded through the intent row, without re-promoting or
redeploying, and repeated recovery keeps one settlement per attempt.

Round three pins the rest of the reconstruction arms and the threading
itself: the real Compose chain honors a supplied deploy operation id and
records its started and bound transitions under it; a deploy that
opened without a terminal record settles as unproven, naming its id; an
unbound terminal record settles the same as a failed one; and a fully
completed pipeline settles as exactly no_source_change with no next
action.

* fix(gitops): journal and reconstruct the deploy-intent refusal

The dispatch refusal added for unrecordable deploy intents dropped the
underlying cause from every operator-visible surface, leaked a corrupt
stored row as a thrown decode error into the deploy-failure arm, and
left the one post-commit arm whose classification recovery could not
reconstruct when the settle write failed in the same window: a bound
attempt with no intent row read as an apply-only completion and
reported false convergence there.

The intent hook now captures the scrubbed cause into the settled
reason, contains the decode failure as a read-back refusal, and
journals a deploy_intent_refused witness row (payload type
DeployIntentRefusedPayload, recorded by the new deployIntentRefused
transition) on every refusal path. reconstructBoundDispatchResult
checks the witness before the intent row, so an unsettled refusal
settles recovery_required with view-target-results and no deploy id,
matching the live arm. The refusal log line names the dispatch
operation id for correlation, and the stale comments claiming the
witness writes at the moment of the filesystem commit now describe the
post-commit interval and its unproven fallback honestly.

* test(gitops): pin refusal cause scrubbing and witness recovery arms

Extend the write-failure refusal test to assert the settled reason
names the journaling failure and the refusal witness row lands. The
simulated journaling error now carries a credential-shaped store URL
so the cause capture is pinned scrubbed in both the durable settled
reason and the server log. Turn the read-back variant into a
genuinely corrupt stored payload (undecodable after_json) so it pins
that the decode error is contained as a refusal rather than surfacing
as a deploy failure. Add the compounding cases: intent journal throws
and the refusal's settle write fails in the same window, leaving the
attempt open with the witness, and startup recovery settles it once
as recovery_required without promoting or deploying again; a
journaled-but-corrupt intent row with a lost settle is reconstructed
from the witness directly in storage, pinning that recovery checks
the refusal before decoding the intent so the corrupt row cannot
strand the attempt; and a refusal whose witness write also fails
still refuses, still settles, and logs that the witness is
unavailable.

* fix(gitops): journal deploy intent before fallible deploy preparation

The deploy-intent hook ran after the policy gate and the recovery
handoff, so a policy rejection or a failed handoff threw before the
intent existed. If the resulting deploy-failure settle was also lost,
startup recovery found a bound dispatch with no intent row and no
refusal witness, and projected the source state: a quiet
no-source-change convergence for a deploy the operator asked for that
never ran.

Move the hook to the top of the deploy branch so durable intent exists
before any step that can still stop it. Recovery already distinguishes
the new state: intent journaled without a deploy_started row settles
as a requested deploy with no deploy record, with a retry next action
and no claimed id. The refusal arm, the manual-apply untracked arm,
and the threaded-id behavior are unchanged; only the position moves.

* test(gitops): pin intent-first ordering in policy and handoff windows

Four dispatch tests for the two fallible steps that sit between the
bind and Compose, now that the deploy intent is journaled ahead of
them:

- Policy-gate block and recovery-handoff failure each settle live as a
  deploy failure with the intent row durable, the refusal witness
  absent, Compose never called, one settlement, and recovery adding
  nothing to a settled attempt.
- With the live settle lost in the same window (policy block and
  handoff failure), startup recovery reconstructs both from the intent
  row plus the absent deploy_started row as a requested deploy with no
  deploy record: blocked with retry, the exact commit, no deploy id,
  never the source projection's quiet convergence. Repeated passes
  keep one promotion and one settlement per attempt.

* fix(gitops): record the deploy request in the dispatch reservation

A store outage spanning the whole deploy branch (intent journaling,
refusal witness, and live settlement all failing together) left startup
recovery with bind-plus-no-intent, which reconstructed as the apply-only
source projection and quietly converged a deploy that never started.

The source_reconcile_started reservation now records, atomically and
before promotion, whether the dispatch asked the pipeline to deploy. The
key is written only when true, so apply-only dispatches and reservations
from before this fact keep the exact payload shape readers already
handle. Recovery consults it only when no deploy intent and no refusal
witness survived: a deploy-requested attempt then settles as
recovery_required naming the missing deploy record, never through the
apply-only projection.

* test(gitops): cover the deploy-request reservation fact in recovery

Adds the compound-outage regression: intent write, refusal-witness
write, and live settlement all fail; after recovery the attempt settles
once under its original id as recovery_required, with one promotion,
zero Compose calls, and no trip through the source projection.

Companion coverage pins the other two readers of the fact: an apply-only
dispatch whose settle was lost still reconstructs truthfully from the
source projection (no key written, no false deploy claim), and a legacy
marked reservation without the key recovers as apply-only without
throwing.
2026-09-14 17:21:18 -04:00
Anso 8cf10c45f0 fix(sso): preserve granular roles across enabled role synchronization (#1875)
Extend the sso_role_sync condition in SSOService.provisionUser so the
IdP-derived role is applied only while the stored role is Admin or Viewer.
Granular roles (deployer, node-admin, auditor) remain manually
authoritative. Replace the misleading inline comment with one accurate
block explaining the setting gate, coarse-role sync, granular-role
preservation, and independent email sync.

Backed by parameterized regression tests for all three granular roles.
2026-09-14 14:41:29 -04:00
Anso 66b79b83f0 chore(deps): bump the all-npm-backend group with 8 updates (#1914)
* chore(deps): bump the all-npm-backend group across 1 directory with 10 updates

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

| Package | From | To |
| --- | --- | --- |
| [openid-client](https://github.com/panva/openid-client) | `6.8.7` | `6.8.8` |
| [systeminformation](https://github.com/sebhildebrandt/systeminformation) | `5.33.6` | `5.33.8` |
| [tar-stream](https://github.com/mafintosh/tar-stream) | `3.2.0` | `3.2.1` |
| [undici](https://github.com/nodejs/undici) | `8.10.0` | `8.10.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.0` | `26.4.1` |
| [eslint](https://github.com/eslint/eslint) | `10.9.1` | `10.10.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.68.0` | `8.69.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.11` | `5.0.0` |
| [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1121.0` | `3.1127.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1121.0` | `3.1127.0` |



Updates `openid-client` from 6.8.7 to 6.8.8
- [Release notes](https://github.com/panva/openid-client/releases)
- [Changelog](https://github.com/panva/openid-client/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/openid-client/compare/v6.8.7...v6.8.8)

Updates `systeminformation` from 5.33.6 to 5.33.8
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.33.6...v5.33.8)

Updates `tar-stream` from 3.2.0 to 3.2.1
- [Commits](https://github.com/mafintosh/tar-stream/compare/v3.2.0...v3.2.1)

Updates `undici` from 8.10.0 to 8.10.2
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.10.0...v8.10.2)

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

Updates `eslint` from 10.9.1 to 10.10.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.9.1...v10.10.0)

Updates `typescript-eslint` from 8.68.0 to 8.69.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.69.0/packages/typescript-eslint)

Updates `vitest` from 4.1.11 to 5.0.0
- [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.0/packages/vitest)

Updates `@aws-sdk/client-ecr` from 3.1121.0 to 3.1127.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-ecr/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1127.0/clients/client-ecr)

Updates `@aws-sdk/client-s3` from 3.1121.0 to 3.1127.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1127.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: openid-client
  dependency-version: 6.8.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: systeminformation
  dependency-version: 5.33.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: tar-stream
  dependency-version: 3.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: undici
  dependency-version: 8.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: "@types/node"
  dependency-version: 26.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: typescript-eslint
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: vitest
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-ecr"
  dependency-version: 3.1127.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1127.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
...

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

* fix(deps): hold tar-stream at 3.2.0 and vitest at 4.x pending type/peer fixes

tar-stream 3.2.1 bundles index.d.ts that imports types from streamx,
which ships no type definitions, so the bundled types shadow the
working @types/tar-stream and break the build. vitest 5.0.0 promotes
vite to a required peer dependency; the backend has no vite install.
Both revert to main's ranges; the other 8 group bumps stand.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 02:52:26 -04:00
dependabot[bot] d15bac1ec9 chore(deps): bump the all-npm-frontend group (#1907)
Bumps the all-npm-frontend group in /frontend with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react) | `12.11.5` | `12.11.6` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.37.0` | `1.41.0` |
| [motion](https://github.com/motiondivision/motion) | `13.1.1` | `13.2.0` |
| [@testing-library/user-event](https://github.com/testing-library/user-event) | `14.6.6` | `14.6.7` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.0` | `26.4.1` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.5` | `19.2.7` |
| [eslint](https://github.com/eslint/eslint) | `10.9.1` | `10.10.0` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.5` | `0.5.6` |
| [globals](https://github.com/sindresorhus/globals) | `17.11.0` | `17.12.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.68.0` | `8.69.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.11` | `5.0.0` |


Updates `@xyflow/react` from 12.11.5 to 12.11.6
- [Release notes](https://github.com/xyflow/xyflow/releases)
- [Changelog](https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md)
- [Commits](https://github.com/xyflow/xyflow/commits/@xyflow/react@12.11.6/packages/react)

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

Updates `motion` from 13.1.1 to 13.2.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v13.1.1...v13.2.0)

Updates `@testing-library/user-event` from 14.6.6 to 14.6.7
- [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.6...v14.6.7)

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

Updates `@types/react-dom` from 19.2.5 to 19.2.7
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `eslint` from 10.9.1 to 10.10.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.9.1...v10.10.0)

Updates `eslint-plugin-react-refresh` from 0.5.5 to 0.5.6
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.5...v0.5.6)

Updates `globals` from 17.11.0 to 17.12.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.11.0...v17.12.0)

Updates `typescript-eslint` from 8.68.0 to 8.69.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.69.0/packages/typescript-eslint)

Updates `vitest` from 4.1.11 to 5.0.0
- [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.0/packages/vitest)

---
updated-dependencies:
- dependency-name: "@xyflow/react"
  dependency-version: 12.11.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: lucide-react
  dependency-version: 1.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: motion
  dependency-version: 13.2.0
  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.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@types/node"
  dependency-version: 26.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: globals
  dependency-version: 17.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: typescript-eslint
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: vitest
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: all-npm-frontend
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 01:51:59 -04:00
dependabot[bot] 1771c3d911 chore(deps-dev): bump @playwright/test in the all-npm-root group (#1906)
Bumps the all-npm-root group with 1 update: [@playwright/test](https://github.com/microsoft/playwright).


Updates `@playwright/test` from 1.62.1 to 1.63.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.62.1...v1.63.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-root
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 01:51:56 -04:00