mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-10-26 05:30:18 +00:00
docs/tutorials-batch-3
2180 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2e0bf078c4 |
docs: retire the standalone known-limitations file (#2022)
Limitations are disclosed on the feature page they qualify and fixed forward. Removes KNOWN_LIMITATIONS.md, the pre-1.0 disclaimer notes that linked to it, and names the Compose v2 plugin and Linux production host in the quickstart prerequisites. |
||
|
|
e0a54258f8 |
fix(gitops): bound concurrency for the startup preflight backfill (#2004)
* fix(gitops): bound concurrency for the startup preflight backfill backfillMissingPreflightEvaluations awaited ensureRolloutAuthorization serially for every live-authorized Blueprint application that lacked stored preflight evidence. Each evaluation can spend the full preflight timeout on registry probes, so a hub with N such applications delayed HTTP readiness by up to N times that timeout: startup awaits the backfill before binding the server. Spread the evaluations over a pool capped at 3, reusing the existing mapWithConcurrency helper. Every failure is contained to the one application that caused it, so a throwing evaluation can neither abandon the batch nor leave the evaluations still in flight writing authorizations beside the HTTP bind. Previously one throw escaped to the startup catch and skipped every remaining backfill. The tally reads stored evidence rather than the returned verdict, since evidence is what derive projects from, and every branch that produces no evidence logs why. Tests: bounded overlap with a peak-in-flight counter, the skip cases for an already-evaluated application and for an authorization that no longer resolves, failure isolation, evidence-based tallying, and one case driving the backfill through the real authorization path over five database fixtures to cover concurrent evaluation end to end. * fix(gitops): contain a per-app binding failure in the startup backfill The candidate filter called liveRolloutBinding unguarded, so a store read that threw for one application rejected the whole batch and startup reported it as a rollout reconstruction failure, naming the wrong step. The binding read now happens inside the per-application callback, under the same guard as the evaluation itself: a revoked authorization stays a silent per-app skip, and a store that cannot resolve one costs that application alone, never the evaluations still in flight. The backfill keeps its own startup step so any failure it still raises is logged as the backfill. Derive stays fail-closed: an app that ends without stored evidence is unchanged, and the tally still reads stored evidence rather than the returned verdict. |
||
|
|
b197c1bbaf |
test(git): correct the v3 pending-blob test name and assert the decoded version (#2003)
* test(git): correct the v3 pending-blob test name and assert the decoded version The test in the pending blob decode branches was named "round-trips the v3 blob with candidate path and inventory". Nothing round-trips: the test hand-builds a v3 payload, encrypts it, and decodes it. encodePendingCompose is the only encoder for the pending candidate and hardcodes v: 4, so the codec cannot produce a v3 blob at all. A v3 row can only come from a database written by an older build, which is why the v3 branch of decodePendingCompose exists: to refuse it rather than read it as a reviewable plan. Renames the test to "decodes a v3 blob with candidate path and inventory", which matches the sibling "decodes a v2 blob without a candidate" beside it. Also declares version on the test-local DecodeApi return type, which listed only the four fields this block reads out of the nine the real signature returns, and asserts the decoded version is 3. That field is the discriminator apply branches on to refuse the blob as LEGACY_PENDING, so pinning it makes the test's name verified rather than assumed. * test(git): tighten the pending-blob decode branch tests Two accuracy fixes in the pending blob decode branches, neither of which changes what the block covers. Drops encodePendingCompose from the test-local DecodeApi type. No test in the block calls it, and the declaration was a trap rather than a convenience: it took four parameters where the real method takes five, the fifth being the plan identity. A test that reached for it to round-trip a payload would have type-checked and then thrown on the missing argument. Removing the member removes the trap, and adding a fifth parameter to keep an unused surface alive would be the same speculative code in a different shape. Renames "falls back to legacy plaintext for unknown shapes" to "falls back to legacy plaintext when the blob has no version marker". The test feeds opaque text, which reaches the plaintext tail because it carries no version marker. It does not cover the paths a reader would assume "unknown shapes" means, namely JSON carrying an unrecognised version or a v2 marker whose files check fails. The name now states the trigger the test actually exercises. |
||
|
|
ab9af21762 |
refactor(gitops): drop the orphaned compose-only digest pin guard (#2002)
The apply-local pin guard validates digestPins after the write, against the rendered model, using digestPinsMatchServiceNames with the service names from buildEffectiveServiceModel. That change left the previous compose-only guard behind with no remaining production caller. Remove composeServiceNames and digestPinsMatchComposeServices along with the isRecord and parseYaml imports that only they used, and drop the now-inaccurate "compose + override" clause from the surviving helper's doc comment. The additionalServiceNames parameter was already redundant: the rendered model is built by running docker compose config across every file in the deploy spec, so override-declared services are present in it already. Also remove the readNodePlatform re-export from artifactResolve, which nothing imported. The function stays in effectiveArtifactContext, where it is still used to read the node's Docker platform. Delete the two tests that only exercised digestPinsMatchComposeServices. The live contract is still covered: the surviving rendered multi-file test asserts the positive case, the unknown-pin-key rejection and the empty-service-set rejection, and the route-level 400 for a pin mismatch remains covered in blueprints-route-validation. Pin validation behaviour is unchanged. |
||
|
|
66dac5d183 |
test(git): name the multi-file pull test for the v4 pending format (#2001)
The test drove upsert -> pull -> apply and asserted the apply succeeded, so the pending blob it exercised was necessarily the current one. The v2 branches in decodePendingCompose exist only to refuse pre-upgrade rows as LEGACY_PENDING, and encodePendingCompose is the only encoder and always writes v: 4. The old title named a format this flow cannot produce. Names the format instead of the test's era, matching the sibling tests that already say "v2 pending blob" and "v4 pending blob". Assertions and production code are unchanged. |
||
|
|
acfcb5567b |
fix(gitops): keep an application addressable when its evidence cannot be read (#2000)
* fix(gitops): keep an application addressable when its evidence cannot be read A remote application whose evidence this hub cannot walk answered 404, the same as one that does not exist, so an operator's only signal was an application that had vanished. It now answers 503 with an `evidence_unavailable` code when the row names the application but the projection cannot be read, and the view says evidence is unavailable and offers a retry. A row that never named the application still answers 404, and an unreadable row stays 404 so the two cannot be told apart. Validating that answer meant walking the projection properly. The detail endpoint and the portfolio now check the same evidence: an artifact qualification the facet and its latest evidence disagree on, a blank generation or identity standing in for a real one, and a status from a newer node all read as unknown evidence rather than as a convergence claim. Convergence itself is now proven from the target evidence rather than read off the rollout status, an Inline Blueprint no longer inherits a Blueprint's claim, and a target the intent withdrew no longer keeps an application in the attention queue. Reading a remote row also authorizes it against the node that owns it rather than the node the caller is viewing, and the phone portfolio says partial when the merge cap dropped rows, not only when a node failed. * fix(gitops): grade unfamiliar evidence as unknown instead of unavailable Two shape validators rejected a vocabulary this build does not recognize, which made a whole projection unusable rather than merely unfamiliar. An unusable projection costs the operator its row: the portfolio shows an evidence unavailable placeholder and the detail route answers 503. That claims the evidence is unavailable, which is a stronger statement than an unfamiliar word supports. A last-known-good unavailable reason is now accepted on its structure and graded as unknown evidence instead, on both sides of the wire. The browser refusing the reason the server had already graded would only have replaced the honest answer with a total read failure, and the two validators would have disagreed about the same payload. A drift identity kind is accepted the same way, so one unfamiliar kind costs that row its label instead of the whole application. The label names the kind it cannot read instead of rendering nothing. The kind list is a total record over the identity union, so adding a kind without wording for it is a compile error, and the lookup is own-property, so an inherited key reads as unfamiliar. A blank reason is still rejected, and the mirror consistency rules still reject an incoherent last-known-good record. Verified: backend and frontend tsc clean; seven backend GitOps suites pass (446 tests); full frontend suite passes (3597 tests); frontend lint reports 0 errors. * fix(gitops): restore the recovery-required rollback scope and drop a dead import The conflict resolution carried this branch's three-member failed-runtime set over the four-member set on the main branch, because this branch had reordered the same three entries and the two edits merged without a conflict marker. A target whose recovery is restoring or compensating reports recovery_required, so it stopped counting as a failed target and the rollback dialog stopped offering the failed-targets scope for exactly the stacks that need recovery. The set now matches the main branch, with the type annotation kept. The existing test covered recovery_failed only, so the scope is now asserted for both recovery states. Also removes a type import the same resolution orphaned. The type checker does not report unused imports, so this surfaced only in the backend lint job. |
||
|
|
aadbd05e06 |
fix(app-store): give every catalogue port exactly one protocol (#2019)
* fix(app-store): give every catalogue port exactly one protocol
Installing certain App Store apps failed with "Sencho could not render this
stack's Compose model to check external networks." The catalogue port mapper
built each spec as `${external}:${internal}/${protocol}`. The bundled
LinuxServer.io catalogue sends `external` and `internal` as strings, never
sends a `protocol` key, and for 19 of its 201 apps carries the protocol inside
the container value, so swag produced `443:443/udp/tcp` and wireguard produced
`51820:51820/udp/tcp`. Docker Compose rejects both with `invalid proto`, the
render returns no model, and the external-network gate refuses the deploy.
Jellyfin, qbittorrent, transmission, syncthing, deluge, and unifi were affected
the same way; they cluster in Networking because that is where the catalogue
documents the protocol in the path.
Replace the mapper with a registry-agnostic normalizer applied at the single
ingest boundary, for both the bundled and the custom-registry path. A string
entry is already in Compose's own syntax and passes through verbatim, so the
custom-registry format is untouched by construction and the YAML emitter stays
the only owner of escaping. A structured entry is read through the host and
container field names registries actually use and gets exactly one protocol. An
entry that contradicts itself, or that names a transport Compose cannot accept,
is dropped with a counted warning rather than emitted as a spec that fails
later and more quietly.
The catalogue cache key is now derived from the registry URL. It was a fixed
key, so changing the registry served the previous registry's catalogue for the
full 24 hour TTL to any caller that did not also bust the cache, which only the
Settings form did.
Make the render failure name its own cause. The resolver already reduced a
failure to a specific diagnosis, but the deploy gate discarded it for a
sentence naming none of the possible faults, and the resolver's own fallback
guessed from a list that a malformed port does not match. The gate now
surfaces the diagnosis, and the resolver quotes Compose's own redacted stderr,
which also carries the timeout and output-cap reasons. A pre-flight refusal is
now recognized by type in the template deploy path: nothing has started, so it
always rolls back, and its message reaches the operator instead of a rule's
paraphrase. Matching that text against the generic rules would otherwise have
suppressed the rollback and left a broken compose file on disk.
Verified against all 331 live catalogue entries and by installing swag and
wireguard end to end: both deploy, their Compose models render, and each port
carries one protocol. Also closes two redaction gaps that surfacing stderr made
reachable: scheme-less registry credentials in an image reference, and keyword
secrets quoted on both sides of their separator.
* fix(app-store): correct the audit findings on catalogue ports and redaction
Three corrections to the previous commit, from an audit of the PR.
The scheme-less credential rule added to the shared log redactor matched
digest-pinned image references. It turned
lscr.io/linuxserver/swag:latest@sha256:abc123 into
lscr.io/linuxserver/[redacted]@sha256:abc123, dropping the repository and tag
from every pinned reference it touched, and reported a secret that was not
there. The redactor has 18 non-test callers, one of them the GitOps source
controller, where the pinned digest is the artifact identity being diagnosed.
Two constraints narrow the rule to actual credentials: the credential must
start a token, so a URL path containing "a:b@c/" is left alone, and the "@"
must be followed by a host and a path, which is what a digest never has after
it. The known cost of that narrowing is recorded in the code: a credential with
no path after the host is not caught, and neither is a password containing "@",
which an image reference cannot carry unencoded.
The catalogue format was chosen by hostname, so anything other than the bundled
API was read as a Portainer v2 catalogue. An LSIO mirror or a caching proxy
answered with a body the Portainer mapper did not recognise, it returned no
templates, and the App Store came up empty with nothing logged and nothing
reported. The format is now decided by the body, and a body matching neither
supported shape raises a message that names both shapes, says the older
top-level array format is not supported, and points at the Registry URL setting.
That message is surfaced to the operator rather than replaced by the generic
fetch failure, and a registry that legitimately has nothing in it is still
accepted.
The deployment sheet split each port spec on ":" and took the first piece as the
host port and the second as the container. For a spec pinning a bind address,
127.0.0.1:8080:80/tcp, it offered a bind address in the port field, showed the
host port as the container port, checked port conflicts against an address, and
on deploy wrote 127.0.0.1:8080, losing the container port and the protocol while
reporting success. One parser now reads the address, the host port and the
container out of every short form, and rewriting substitutes only the host-port
segment, so the rest survives by construction. An address is recognized by what
it is not, since a catalogue can publish a hostname there as readily as a dotted
quad. A port range is no longer placed in a field that accepts one number, which
had been blocking the install, and it stays visible rather than being dropped. A
cleared or non-numeric field leaves the registry's spec alone.
Verified against a custom registry publishing the shapes the bundled catalogue
never sends. The sheet offers 8080 beside 127.0.0.1:80/tcp, offers no field for
a range or a container-only port, and an edited host port deploys as
127.0.0.1:9090:80/tcp with the bind address, container port and protocol intact.
An unrecognized body now reports its reason in the App Store instead of showing
an empty grid. The bundled catalogue is unchanged, and an install that collides
with a port already bound on the host still rolls back cleanly.
* fix(app-store): leave an unbracketed IPv6 port spec exactly as published
A Compose short form can pin a bind address without brackets, and Compose
accepts it: "::1:8080:80" is read as address "::1", published port 8080,
container port 80. Splitting on colons cannot find the boundary, because
neither "::1" nor "::1:8080" can be ruled out as the address, and the previous
parse reported an empty address, so the deployment sheet offered 8080 and
displayed "8080 : :80" with the bind address hidden.
A spec whose leading region holds more than one colon is now left exactly as
the registry published it: no port field, no conflict check, and no rewrite.
The sheet shows the published text whole, without the separator that would
imply a host and container split it does not have. Every unambiguous form is
unaffected, because a valid unbracketed spec has at most three fields and a
bracketed one is consumed before this check.
Also record, next to the rule it applies to, that the credential matcher
over-matches text shaped like a time and a host, "10:30@host/a". That shape is
indistinguishable from a numeric user and password, and the direction is
deliberate: an over-redacted log line costs a word, an under-redacted one
leaks.
|
||
|
|
248154a2d3 |
fix(blueprints): gate digest-pinned remote apply on leaf capability (#2005)
Enforce repairs a frozen image-digest disagreement by redeploying the stack at the approved digest with the pull suppressed, so a drifted image cannot come back under the same tag. For a remote target the hub carried those pins in the apply request, but it never checked that the node receiving the request understood them. A node running a release that predates digest pins drops the field and redeploys by tag anyway, which is the one outcome the pin exists to prevent: the drifted image is pulled again, and the row is marked active while the drift is still there. The existing mixed-version guard on that request only fires when the node lacks the atomic apply endpoint entirely, so a node in between the two behaviours was never caught. The hub now probes the target for a new capability and refuses to send a digest-pinned apply to a node that does not advertise it, or whose metadata it could not read. The refusal is the same upgrade-required error the missing-endpoint case already raises, and it lands before the delivery contract is negotiated, so nothing about the apply leaves the hub. Applies that carry no pins are untouched, so nodes that never take part in a digest repair need no upgrade for them. The capability is advertised through node metadata, so an upgraded node starts serving pinned applies with no further configuration. |
||
|
|
32fd95dd57 |
feat: health-gated Blueprint rollout policy (#2018)
* feat: health-gated Blueprint rollout policy
A rollout policy that reacts to each target's health outcome instead of
deploying the whole fleet blind. Five modes, applied one target at a time
under any non-observe policy:
observe record the outcome and keep deploying the rest
pause hold the rollout before the next target
retry_once deploy the same generation one more time
stop hold, and leave the failed target where it is
rollback put the failed target back on the generation it ran before
The policy is frozen into the rollout generation at authorization, so a
fleet is never halfway between two policies, and it is carried across
Blueprint edits. Observe is the default and is exactly the old behaviour.
How it works:
- A health run is reserved, with the whole rollout binding, before the
apply goes out, so a lost response still leaves a durable record.
- The next target is the first required node not settled by health, not
the first unacked one, so a fleet cannot run ahead of a check.
- The verdict drives a pure decision function; the executor carries the
result out afterwards, so the state is durable before anything is
applied or restored.
- `unknown` never retries, stops, or rolls back. Missing evidence is
held for a human, not acted on.
- Remote targets are refused for non-observe policies before the first
apply, because this instance cannot produce a health verdict for a
remote node's Docker.
Permissions: the policy write and the controls take `stack:deploy` at the
application, and every target the policy can act on is checked exactly.
* fix: hold before a policy restore and never lose its record
Five review findings on the health-gated rollout policy, all on the paths
the policy exists to make safe.
A completion that could not be recorded was swallowed. `holdRollout` ran
only after `restoreTargetToGeneration` returned, so a `rollbackCompleted`
that threw left the outer catch with nothing to do: no hold, no partial
record, and the target stuck in `restoring` while the fleet carried on as
if the policy had not stopped it. The restore and the record of it are now
one unit, and any failure records a partial failure on the target *and* the
application, since `rollbackInProgress` opened the application into
`restoring`.
The hold now precedes the restore rather than following it. A restore is
external and slow, and until it returns the rollout is still authorized, so
an operator dispatch or an auto-accept could deploy the next target, or
this one again, while it was in flight.
A decided rollback was indistinguishable from a pause, so a crash between
deciding and doing lost it: the fence read as a question, a resume answered
it, and the rollout moved on past a target still on the failed generation.
It now has its own reason, which no resume clears.
A resume re-paused on every restart. A kept fence is also what reconstruction
holds on, so a rollout the operator had already answered for was held again
each time. A resume now records the answer as `stop_acknowledged`, which
still keeps the target out of the queue but no longer holds the rollout.
A policy rollback passed null captured refs, which cleared the source
acceptance the restored generation was bound to and recorded it unprovable.
That is source-authority state, and a health verdict has none over it.
A blocked follow-up dispatch now holds the rollout with the reason it was
refused. There is no scheduler, so that dispatch is the only thing that
brings the next target.
Also: the policy route required the fleet-wide deploy grant on top of the
per-target checks, unlike the pause and resume routes beside it, so an
operator whose grants are scoped to specific stacks could pause and resume
but not configure the policy. The fleet-wide grant is now required only
when there is no target to check.
The rollback happy-path test asserted only that the restore had been
*called*, and passed while `rollbackCompleted` threw inside the executor
because the fixture's generation row was fake. It now seeds a real one and
asserts the completion, plus the completion-failure, restore-throws, and
source-acceptance cases.
* fix: a pending roll back stops the rollout, not just its own target
Excluding a `rollback_pending` target from the queue is not enough. The
queue steps over it and deploys the next one, so a fleet advances past a
target still on the generation that failed, with no restore done and
nothing telling the operator. Resuming did not help either: it left the
fence in place and unpaused the application, so the next dispatch moved on
and the following restart paused the rollout again, repeating for as long
as the operator alternated the two.
A gated dispatch now refuses outright while any target of the current
rollout generation is `rollback_pending`, and the refusal names the manual
restore route as the way out. The fence stays where it was, so the target
card still reports it.
The test now uses two nodes and dispatches after the resume, so the
step-over is visible; it is verified to fail without the change.
Also corrects the schema comment, which claimed a restart mid-rollback
"reconciles the rollback". It fences the unfinished restore and does not
replay it, which is what the comment says now.
|
||
|
|
91648b049d |
fix(gitops): keep Stack Drift, notifications, and metrics on the canonical posture (#2011)
* fix(gitops): keep Stack Drift, notifications, and metrics on the canonical posture Three surfaces consumed GitOps application status without going through the posture read model, so each could contradict the portfolio about the same application. The portfolio, the application detail panel, and the Blueprint surfaces were already on it; these three were not. Stack Drift rendered only the node-local revision, which has no posture and no evidence freshness. Every facet card could read satisfied with no drift listed while the portfolio reported the application unsettled, because the evidence was stale, a node had gone dark, or a target was unreachable. The tab now reads the hub-owned portfolio row by application id and renders the posture, the evidence notices, and per-target evidence freshness, and each drift item shows the age of the observation behind it. The read is keyed on the application id the projection already carries rather than on a stack name, because a Blueprint application has no stack name of its own and two applications can share a deploy stack. A settled source attempt whose outcome is unproven notified as a pull failure at error level, while the portfolio reported the same application as in-progress. Unproven is now its own category at info level, saying the attempt settled without proving a result. Genuine failures are unchanged. A recorded health-check failure produced no notification at all, because the finalize stage was not notifiable, so a known-bad workload left the portfolio reporting a failure with nothing behind it in the bell. A recorded failure now notifies, and only a recorded failure: a passing check, a check that could not reach a verdict, a verdict that could not be tied to the running stack, and a failure recorded against a generation a rollout has already superseded are all silent. The health transition now records the verdict it actually persisted, which is what the notification keys on, so the bell and the projection cannot disagree about which failures count. Metrics was audited rather than changed. Its counters key only on transition stage and outcome, both closed unions carrying no application identity, so no count derived from them can be read as a claim about an application. That is now pinned by a compile-time exhaustiveness assertion and a behavioural test, because a settled word reaching that union is the only way it could start lying. * fix(gitops): stop the posture consumers contradicting the portfolio Follows review of the first attempt at this change. Four defects, three of them introduced by that attempt and one pre-existing. The health notification was a duplicate. The only producer of a recorded GitOps health verdict is the health gate, which announces the failure as it records it, so a failed run produced two bell entries saying the same thing. The GitOps outbox now stays out of it, and a test pins that a recorded failure yields exactly one notification and names where it comes from. The unproven reconcile outcome now does not notify at all, rather than notifying at a lower level. A settled attempt that proved nothing is evidence the portfolio reports as in progress or unknown, and the criterion is that the bell must not report on such evidence. What is unproven is the portfolio's to show, not the bell's, so the row is suppressed at the point the outbox decision is made. The insert and the drain carry the same transition record, so the two cannot disagree about which rows exist. The posture card warned falsely in two cases. A detached stack still projects a revision, so an identity was built for it and the portfolio route answered 404, producing a permanent warning about an application the portfolio does not list. A Blueprint stack read by a caller without the fleet-wide node grant answered 403 and produced the same permanent warning for a stack that caller is entitled to read. Both now say nothing: the route answers 404 for missing and not-granted alike on purpose, so neither status can honestly be reported as a fault, and the tab keeps the node-local view it always had. The unreadable warning is now reserved for the one case that is a real fault, a permitted read the server could not answer. The metrics invariant test claimed to be an exhaustiveness tripwire while only checking that its listed values were allowed. It now also asserts no unlisted member exists, verified by adding a value and watching it fail. Tests for the new hook cover the answers per outcome, the hub-only read, a slow answer for a previous identity being discarded, and the announced refetch with its debounce. Backend 1025 GitOps tests, 479 neighbouring tests, and 3506 frontend tests pass; both typechecks, both builds, and both lints are clean. * fix(gitops): scope the posture read and drop the report that no longer fits Follows the non-blocking findings on the posture work. Six items; four changed code, two are recorded as needing no change. The change announcement names the application it came from, so the posture read filters on it rather than treating every GitOps event in the fleet as this application changing. That was wasted work and a card that flickered on activity it had nothing to do with. An announcement that names no application is not filtered, because it cannot be attributed and dropping it could miss a change to this very application. A node change left the previous node's report in state for the length of the new request, so the new node was composed with the previous node's application and one identity that cannot exist was probed. Clearing the report was not sufficient on its own: the render that follows the node change already sees the old report, because the effect that clears it runs after that render. The report now records the node it was read from, and the posture read is skipped unless that is the node now selected. A test drives the switch and asserts the mismatched request is never sent. Work in flight and cannot-prove shared a glyph. They are the two postures most easily read as one state from a distance, and they are exactly the two that must not look alike on the surface an operator opens to find out which it is, so each tone now resolves its own icon. Boot repair drains rows an earlier process inserted, so a build predating the unproven suppression can leave one behind. The repair now declines it too, rather than announcing a failed pull the current insert would refuse to write: the one path meant to reproduce the original outcome must not be the path that diverges from it. The row is still drained, and the suppression is logged so a silent entry is a recorded decision rather than something a reader later has to explain. The health gate being disabled mid-run, which leaves a failed verdict announced while the projection reports health as not applicable, needs no change here. The entry it describes is the health gate's own, and the GitOps notification that would have doubled it is gone. The reclassification of a stored row is no longer intended but is no longer reachable either, since the category it was reclassified into does not exist. |
||
|
|
3f2b5348be |
fix(e2e): stop a failed two-factor spec from cascading the whole suite (#2009)
The E2E suite runs serially against one shared admin account, so a spec that leaves server-side state dirty breaks every spec scheduled after it. The two-factor spec was the worst offender: it enrols, consumes backup codes, then disables, and its cleanup only ran when the block had captured both a secret and two unused backup codes, with every error swallowed. A test failing mid-chain destroys exactly that state, so MFA stayed enrolled and each later login stopped at the challenge screen, paying the full readiness timeout three times per test with retries until the job hit its ceiling. Playwright also retries a serial block as a whole in a new worker process, so the retry could not re-enrol either: the enrol endpoint answers 409 for an already-enrolled account and the group failed again. Reset the enrolment from beforeAll and afterAll through the existing emergency CLI, which is the only path that does not require possession of a second factor. The block is now self-healing on a retry and cannot leak state. Two supporting changes: - loginAs now recognises the challenge screen and a login that never left the sign-in form, and names the cause instead of timing out. A cascade costs seconds per test and says what to do about it. - The suite bounds itself in CI, so a cascade fails the test step while the job is still alive to upload the report. A job cancelled by its own timeout runs no further steps, which is why the previous runs produced no evidence at all. The job ceiling is raised above that bound as a backstop. |
||
|
|
ebbd504a5a |
test(e2e): fix the file-explorer Rename locator collision and log failed requests in deploy specs (#2017)
The context-menu assertion matched both the Rename item and a leftover lifecycle-rename-dst.txt row from an earlier test, failing the first attempt in most CI runs. Match the menu item by role instead. The deploy-feedback specs now log aborted requests and attach the browser logger to the inline-progress group too, to diagnose an intermittent login failure after deploy tests. |
||
|
|
be9d1eacf2 |
ci: cut backend job time by removing real timeout waits and splitting Docker integration (#2016)
Prune route tests waited out real 12s withTimeout budgets and the monitor breaker tests waited real 8s janitor timeouts. Route tests now cap the budget through a test helper and assert the budget each call requested; the breaker tests advance fake timers. The Docker integration tests run in their own job alongside the unit tests, with an aggregator keeping the required check name. |
||
|
|
a5b938ab93 |
ci: shard E2E, skip docs-only PRs, and drop the inert visual-regression gate (#2013)
* ci: shard E2E, skip docs-only PRs, and drop the inert visual-regression gate The visual-regression workflow never had committed baselines, so every run skipped its comparison and passed. Remove it with its spec and Playwright project. E2E now runs as four parallel shards, each with its own app and database, and no longer waits on the backend job. A single aggregator job keeps the required check name. CI retries drop from 2 to 1 so a real failure no longer runs three times. A change-detection job skips the build, test, and image jobs when a PR only touches docs/, .claude/, .opencode/, or markdown. It fails closed: an empty file list, a detection failure, or a rename out of code runs everything. * test(e2e): create the test admin in global setup so specs do not depend on file order Sharded runs start each shard on a fresh instance. A spec that logs in through the API in beforeAll failed with 401 when it ran first, because the admin was previously created by whichever earlier spec clicked through the setup screen. |
||
|
|
bcc4b7e00a |
fix(frontend): stop client-side preference changes refetching all settings (#2008)
* fix(frontend): stop client-side preference changes refetching all settings The Playwright suite was failing intermittently with HTTP 429 from the global API limiter. Instrumenting a real run showed why: 22,177 API requests over 19.6 minutes, 58% of them 304 Not Modified, and the shared admin bucket averaging about 970 requests/min against its 1000 ceiling, so ordinary bursts were rejected. The failures surfaced far from the burst, as the stacks view failing to load and the Create Stack button never appearing, which reads like an unrelated UI fault rather than a rate-limit problem. GET /api/settings was a quarter of all traffic, peaking at 461/min with 3,810 of 4,781 responses being 304s. useDeveloperMode treated a settings-changed event with no changedKeys as "refetch", and fourteen dispatch sites across ten hooks broadcast with no detail at all. Every one of them is a client-side preference backed by localStorage: pane layout (four sites), density, top-nav alignment, mode and labels, log chip colours, deploy feedback, quick links. So dragging a panel divider or nudging the top nav pulled the whole server-side settings document over the network for a change with no server-side effect. useDeveloperMode now refetches only when changedKeys names developer_mode, matching the two listeners that already gate this way. All three server-side write sites dispatch changedKeys, so a real developer_mode toggle still propagates immediately. The non-production ceiling of the global limiter moves to 10000 so the suite, which shares one account across every spec, stops tripping it. Production is untouched at 200, still configurable with API_RATE_LIMIT. The two changes are complements: this removes a real defect, the ceiling gives the suite headroom. * test: rescale global rate-limit assertions to the new non-production ceiling The pilot enrollment suite asserts which limiter applies by reading the ratelimit-limit header, and hardcoded the global tier at 1000. That value moved, so the assertion failed in CI. The enrollment tier's own ceiling of 100 is unchanged, so the test still proves that a proxy-mode node creation falls back to the global tier rather than the enrollment tier. Two floors in the rate-limiting suite were missed in the previous commit. They read at least 990 remaining, which against a 10000 ceiling no longer constrains anything: the assertions passed for a reason unrelated to what they were checking. Rescaled to 9990 so they keep their original meaning, that at most ten requests were spent from the shared budget. Note that these two assertions were already weak before this change: one request per user leaves 9999 remaining when the buckets are independent and 9998 when they are shared, so a bound of 990 admitted both. They still do. Tightening them to 9999 would make them prove the independence their name claims, which is worth doing separately. |
||
|
|
b4d01da700 |
fix(gitops): fail closed on incomplete target evidence (#1998)
* fix(gitops): fail closed on incomplete target evidence * fix(gitops): harden portfolio evidence validation Keep tombstoned target history out of live posture, attention, and counts. Require complete cross-facet proof before reporting convergence, and fail closed on malformed remote projections. * fix(gitops): let converged apps settle on real evidence Convergence required every current target to report a reachable connectivity, but that column is seeded to null and no producer ever wrote it. Every target read back unknown, so no application could ever show as converged or qualified in a real install; the portfolio and its header could only ever say unknown. Fixtures that hand-built the value are the only reason this was not visible. Connectivity is now projected from the target's own recorded runtime observation, which exists precisely because the node was reached. A stored negative claim is still honored verbatim, so it can only withhold a settled claim, while a stored positive must be earned by a real observation. There is deliberately no wall-clock window: nothing re-observes a Direct target on a timer, so a time window would silently return a healthy, untouched application to unknown. Making convergence reachable exposed a second fail-open that had been masked by it. A failed health verdict returned early without clearing the promotion it had previously earned, so a target kept reporting healthy after a check had failed. It now withdraws that claim, matching on generation and scope, and only for a failed verdict: the health gate records unknown for benign conditions such as a healthcheck still starting when the window ended, and it re-arms against the already applied generation on every update, so revoking on those would leave a healthy application unsettled with nothing re-verifying it. The last known good pointer is left alone as a high-water mark and rollback candidate rather than a live health claim. Tests drive a real Direct application through the whole transition chain and assert the posture the portfolio would show, so the fail-closed rule is pinned in both directions: reachable evidence settles, a missing observation or a failed verdict withdraws it, and a qualified application settles too, which shows the hub-side re-verification agrees with real derive output. The previous settle-only fixtures that stored a reachable value are updated to record a real observation, so they assert what their names claim. * fix(gitops): keep a settled claim from outliving its evidence A failed health check read as work in progress rather than as a failure. The health facet had no failed branch, so withdrawing a failed check's promotion left a known-bad workload indistinguishable from one that had never been checked, and the portfolio showed it as in progress for ever. The last stack-scope verdict is now persisted on the target row, bound to the generation it judged, and the facet reports failed for it, so the posture becomes failed with the health_failed reason that was already declared but never reachable. An unknown verdict is recorded only when nothing is recorded yet, and never overwrites an existing one. The gate reports unknown for benign conditions such as a healthcheck still starting when the observation window ended or a newer operation superseding the run, so letting it clear a failure would resurrect the same in-progress-forever state, and would apply to a failure the opposite of the rule already applied to a passing claim. A Blueprint row also kept claiming it settled after the node holding it stopped answering. A Blueprint target is projected hub-side from the hub's rows, and a recorded observation is a statement about the last time that node answered, so a dark node left the row settled for ever. The node probe now runs before the Blueprint rows are built, its results are reused for the remote Direct rows rather than probing every node twice, and a node that could not be reached marks that row's live targets unreachable. Only genuinely unreachable nodes are overlaid: an unsupported node answered but could not be read, which is a different claim and would unsettle every Blueprint on an older node. The row still derives its own partial set from the overlaid targets, so no second derivation of that fact is introduced. Three columns are added to the target row for the recorded verdict, with the same CHECK on the additive migration that the fresh-install schema carries, since SQLite accepts a CHECK when adding a column. Tests cover both directions of the verdict: a failure is reported as failed and stays failed across a later unknown verdict, a first unknown verdict is kept, and a Blueprint target on a node that stops answering reports unreachable with the node named in its evidence. * fix(gitops): keep Blueprint detail in step with its portfolio row A Blueprint application row is projected hub-side, and its recorded runtime observation is a statement about the last time that node answered. The portfolio list learned to overlay reachability onto Blueprint targets, but the application detail panel built its row from the raw projection and never probed. A Blueprint whose node went dark therefore showed a failure in the list while its own panel still claimed a settled state, which is the contradictory public truth this read model exists to remove. The detail panel now asks each of that application's remote target nodes whether it answered and applies the same overlay the list uses. Only nodes the portfolio also probes are asked, through a shared set of probeable node ids that both surfaces now derive from, so the hub itself and a target whose node row has been deleted are excluded from both. Excluding the hub is what makes this safe on a single-node install: a probe aimed at the hub answers "no proxy target", which is indistinguishable from a node that did not answer, and would have withdrawn the settled claim on the most common shape there is, a Blueprint applied to the hub it runs on. A node that produces no probe result also no longer takes down the page. It degrades to a coverage entry instead, matching the promise the surrounding probe legs already make that one bad node never fails the whole portfolio. Two route tests cover the agreement from both sides: a Blueprint on the hub itself stays reachable in the list and the detail panel, and a Blueprint on a silent node reports unreachable in both with the attention reason named in both. Each fails when its own fix is reverted. |
||
|
|
cb6f7bd512 | test(gitops): correct acceptance evidence semantics (#1999) | ||
|
|
61b25f48d2 |
feat(gitops): show per-target image digests and document the drift model (#1984)
* feat(gitops): show per-target image digests and document the drift model The observed per-service digests already reached the projection, but the approved side did not, so a reader could see that two identities differed without ever seeing which digest each side held. The frozen artifact set's per-service evidence now rides along with the expected identity, and each target's card compares them per service: the approved digest beside the running one, the approved child for the platform that node actually runs, and matched, drifted, unverified, or not compared as the honest answer. The comparison follows the model's own rules, so the view cannot claim a match the projection does not, a stale or unqualified identity stays unverified, a built service is labelled not compared rather than matched, and a value that is not a content digest is ignored instead of compared. Adds the drift model reference the model was missing: the seven classes and what each is measured against, how to read an item, where each surface shows it, the states that are progress rather than drift, what Sencho cannot prove, and the four policy domains. The Blueprint page gains the digest-drift section it never had, trimmed to the Enforce mechanics. The end-to-end spec pins an approved digest, replaces the running workload with different content under the same project, and asserts the deployment reports the drift, the projection carries both digests, and the Drift tab renders the comparison without adopting what the tag now points at. * test(gitops): pin the digest drift spec to a resolvable repository The spec pinned a locally invented repository, e2e-digest-probe:<stamp>@sha256:..., which is not a reference any registry knows. That works or fails depending on what the local Docker version decides to do with a tag-and-digest reference that has no matching repo digest, and the observation filters the running image's repo digests by the authored repository, so an invented repository can never produce an observed digest at all. On a runner whose Docker resolves it differently, the deploy never lands and the spec waits out its poll. Both sides are now real upstream tags of one repository, so the approved reference resolves from the local content store without a registry call and the moved side is a real digest difference. The digests are read from the daemon per repository rather than trusting Docker's ordering, and the two sets are asserted disjoint before anything runs, so two tags that resolve to one manifest fail by name instead of by timeout. The spec also stopped assuming things it never checked. The baseline is proven from the daemon, because the container that was actually created from the approved reference is the ground truth for what is running, and reading it costs one call where waiting for the projection's observation costs a reconciler tick. Every poll now asserts its status, so an API error reports itself instead of surfacing as a timeout, the apply retry is limited to the stale-preview case the UI asks the operator to confirm again, Docker calls are bounded, teardown clears its handle only after a confirmed delete and then checks the containers are actually gone, and a Docker that is installed but broken fails instead of skipping. Verified against local dev servers across four runs at 16 to 55 seconds each, with no leftover images, containers, or override files, and type-clean under a strict invocation. * test(gitops): prove Enforce repairs to the approved digest, not the moved tag The digest drift spec proves detection. It does not prove repair, because it runs the Blueprint in observe mode, and the Inline path is not the one that repairs. The Git-managed loop re-observes a drifted target and applies the configured drift mode, so that is where the repair mechanic lives, and it now has a test of its own. The test starts from a genuinely drifted target rather than an assumed one. The fixture seeds a newer candidate artifact set carrying a moved digest, points the latest pointer at it while the approved and last-known-good pointers stay on the original, and records a runtime observation carrying the moved digest. That starting state is asserted before repair runs, so a fixture that quietly stopped seeding it fails instead of passing on a correct-looking end state. Repair then has to do the one thing that matters: hand the deploy the approved platform child digest, take it from the accepted artifact set rather than the most recent resolution or the running image, and leave every pointer that says what was approved exactly where it was. The observation still reads as drifted afterwards, because repair changes what runs, not what was agreed. The test is mutation-checked. Pointing repair at the latest artifact set instead of the expected one makes it fail with the moved digest, which is the bug this exists to catch. The taxonomy page also records the settings-only reconcile decision properly: source drift compares commits, so a change of compose path, env path, or context directory creates no expected-versus-observed pair and no drift item. It surfaces as reconcile required, and for a Direct application that is where you fetch again. The page says plainly that this is a choice about what the word drift means, not proof that the materialized content was unchanged, because changing where Compose is read from can change the content. The portfolio page no longer keeps its own copy of the class list. It names the classes by reference to the drift model page, so the two cannot disagree. Verified: backend typecheck and lint clean, 9515 backend tests passing, frontend typecheck clean with zero lint errors and 3457 tests across 347 files, docs config parses. * test(gitops): pin a settings-only reconcile as attention, not drift The decision is recorded on the taxonomy page: a change to the compose path, env path, or context directory, with the accepted commit still matching the ref, creates no source drift item. Source drift compares commits, and that shape has no expected-versus-observed pair of commits. That decision needed a test to hold it, and the coverage that existed asserted the facet status and the offered action without ever asserting that no source item is emitted. The behavior was therefore unpinned in both directions: nothing would have failed if the producer started reporting it, and nothing would have failed if it stopped. This adds the missing half. The fixture is the decided case exactly: same commit, different materialization fingerprint. No source drift item, the facet reads reconcile required, and fetch is offered. It is mutation-checked, and dropping the commit comparison from the producer makes it fail, so it holds the line rather than merely restating it. |
||
|
|
e1c044d05b |
fix(exec): fall back to /bin/sh when a container has no bash (#1997)
Opening a shell on a container without bash (Alpine, BusyBox) failed with "exec: /bin/bash: no such file or directory" instead of falling back to /bin/sh. With a TTY attached, Docker accepts exec creation and start for a missing binary and only reports the failure as stream output, so the try/catch fallback never ran. Probe /bin/bash then /bin/sh with a short non-interactive exec and pick the first that exits 0. Exit 126/127 means the shell is absent; any other probe failure (timeout, daemon error) is reported with its real message instead of a misleading "no shell", and the socket is closed on failure. Also replace the empty catch on the Host Console Copy button with a log and an error toast. |
||
|
|
423f4e9f11 |
fix(terminal): support copy and paste in container and host consoles (#1996)
Right-click in the container bash modal and the Host Console did nothing useful, and Ctrl+V was sent to the shell as a literal ^V, so there was no reliable way to paste. - Right-click copies an active selection, otherwise pastes from the clipboard in secure contexts. Over plain HTTP the native menu is kept and the terminal is focused so its Paste entry reaches xterm. - Ctrl+V, Ctrl+Shift+V and Shift+Insert skip xterm key translation so the browser's native paste event fires. - rightClickSelectsWord is disabled so macOS right-click pastes instead of always selecting and copying a word. - Clipboard read or copy failures surface a toast; the selection is only cleared after a successful copy. Closes #1988 |
||
|
|
698d3f34bc |
feat(gitops): produce the remaining drift classes (#1979)
The revision projection only emitted runtime and rollout artifact drift, so the other four classes named in the model never appeared. Source, placement, managed-project, and health now have producers, each keyed off the facet it belongs to so a staged candidate, a pending authority decision, a queued rollout, a mid-apply manifest cache, or a mid-deploy target emits nothing. The invocation class stays absent: the observed invocation is never persisted, so producing it would fabricate drift on every application. |
||
|
|
3ecb954f28 |
fix(gitops): guard automatic acceptance against stateful withdrawal (#1975)
* fix(gitops): guard automatic acceptance against stateful withdrawal Automatic GitOps acceptance had no stateful check: a candidate that removed, renamed, or stripped the volumes of a stateful service could be accepted and applied unattended, destroying or orphaning that service's data. The poll acceptance path and the automatic webhook apply path now share one guard that diffs the candidate's stateful service set against the generation in force, reading managed evidence only. When the candidate withdraws a stateful service, or when that cannot be proven, the candidate is held for review: nothing is accepted or applied, a durable block reason is recorded, and the projection names the block as its own attention reason, so the workplace shows it instead of an unexplained review. Webhook auto-apply reports the hold in its delivery result. Repeat pulls of the same held commit reuse the staged candidate, so the hold survives polling instead of churning generations, and a stored redelivery keeps its automated authority even if the policy moved since. First acceptance under a source with no accepted generation still defines the baseline, since there is no managed state to compare against. Validation: backend tsc, 9479 backend tests, frontend tsc, lint, and tests, with new coverage for the poll hold, rename, non-withdrawing acceptance, no re-evaluation, unreadable evidence, the webhook hold, a stored redelivery after a policy change, and repeat-pull reuse. The full backend run has one pre-existing environment failure in the SSH integration suite because port 22 is already in use on this machine. * security(gitops): contain generation reads under the managed root CodeQL flagged three path-injection sinks in the new stateful guard: the staged compose reads, the staged .env read, and the generation-in-force compose reads each joined a row- or user-derived relative path into a read without a containment check the analyzer can see. Each read now resolves its target and checks it inline at the sink, the pattern the repository already uses where CodeQL credits the guard, and every generation directory is contained under the managed root before anything inside it is read. Escapes are treated as unreadable evidence, so the guard holds rather than reads outside the managed area. No behavior change for legitimate paths. * test(gitops): cover stateful guard edge cases and review block lifecycle Pin the guard's hold on stripped volumes and unparseable candidates, the first-acceptance baseline, and every exit from the durable review block, including that automatic acceptance resumes once a newer candidate clears it. |
||
|
|
13690c01d2 | feat(gitops): connect the workplace to stacks, Git sources, Blueprints, and Fleet (#1978) | ||
|
|
09494d2f95 |
feat(gitops): harden the portfolio workplace and scope stack indicators (#1977)
* fix(gitops): keep failing applications inside the portfolio merge cap * fix(gitops): repair phone search and paging, bound the attention queue, add clear filters * feat(gitops): open the portfolio scoped to a stack from its Git indicators * fix(gitops): list hub-local legacy Git sources and reach hub-only views from a remote node |
||
|
|
39e132512f |
fix(db): log failed column migrations and cover poll-column upgrades (#1976)
initSchema's column helper swallowed every ALTER TABLE error, so a real migration failure left no trace and surfaced later as an opaque crash. It now ignores only duplicate-column errors and logs anything else with the table and column. Adds a regression test that starts from a gitops_applications table missing next_poll_at and poll_interval_secs and asserts startup restores both columns and the due-scan indexes. |
||
|
|
8ff0f51a1b |
fix(ui): polish modal footer, quick-link removal, and empty Recent alerts (#1973)
* fix(ui): keep modal actions right-aligned when the footer wraps * fix(navigation): name the quick-link remove action explicitly * fix(dashboard): let an empty Recent alerts panel collapse when it stands alone The eight-row reserved height only matters when the panel sits beside Fleet Heartbeat; on a single-node install it left a large empty card. |
||
|
|
1347bca5e5 |
fix(fleet): stop advertising a dev build the node already applied (#1974)
The dev-build availability digest was recorded without the running image it was measured against, so after an update recreated the container the Fleet card kept offering the update it had just applied until a later check was conclusive. Availability is now bound to the running image id. Mobile Fleet's dev-build update action also now requires the node to be online with no update in flight, matching the desktop card. |
||
|
|
fc6cc62a27 |
fix(ui): tighten Stack Health row semantics and resizable pane affordances (#1972)
Stack Health rows no longer nest the GitOps badge button inside a button-role row; the stack name is the keyboard target. Sort headers announce direction and show a focus ring. Resize handles reset to the default width on double-click and stay highlighted while dragging. Modal bodies size against the dynamic viewport height. |
||
|
|
48b0276fab |
feat(gitops): notify on authority, rollout, and stateful-hold decisions (#1969)
* feat(gitops): notify on authority, rollout, and stateful-hold decisions Extend the existing GitOps outbox to a second payload kind so a committed transition becomes a notification-history entry for the decisions an operator acts on, not only for a settled source attempt: - source acceptance, placement approval, and rollout authorization; - rollout pause, resume, and supersede; - rollout rollback start, completion, and partial failure; - a stateful deploy held for its first-deploy confirmation. The v2 payload carries identities and the recorded reason only. The stage to category/level/message mapping is closed in notifications.ts, one distinct category per stage, and the insert and the drain share one predicate so the two cannot disagree about which rows exist. Categories are history-only, like the git source attempts: they reach the bell and the Activity timeline and are never dispatched to external channels. An application-level pause now records its operator reason in the history delta, and a notification names the Blueprint when an application has no stack name of its own instead of showing the application id. * fix(gitops): scope and harden the notification fanout Review follow-ups on the notification stream: - one gitOpsOutboxPlan decides which stages write an outbox row, called by both the insert and the drain, so the two cannot drift; - a Direct source acceptance is the controller's automatic bookkeeping and is excluded, leaving the operator acceptance notification to Git-managed Blueprints while the settled attempt already reports the change; - the drain dispatches on the exact payload version and reports an unknown version with a kind-neutral code, and its body is total so a storage error on the macrotask cannot end the process; - extra history fields can no longer overwrite the compact application snapshot, and error text is sanitized before it reaches a log; - docs state the mute-rule and bell behavior truthfully. * refactor(gitops): share the outbox insert and drop the unused reason arm The two payload kinds only differ in the encoded JSON and version, so one insert helper holds the SQL. The notification reason reader now reads the pause key only, which is the only free text a notifiable stage records today. * docs(gitops): scope Blueprint notification entries correctly Placement approval is recorded for any Blueprint, including an Inline apply, while source acceptance and rollout authorization are Git-managed only. The notification label names the Blueprint for these categories; the stack form belongs to the settled source attempt. |
||
|
|
0ab26529d3 |
feat(dashboard): rebalance the Home lower half around operational signals (#1953)
* feat(dashboard): rebalance the Home lower half around operational signals Home now reads as a triage: Recent alerts leads the operational row, with Fleet heartbeat beside it on multi-node installs, and Configuration summary closes the page. Every card below the gauges renders through one shared panel shell and the shared Table primitives; actionable rows stay real table rows with a native button in the primary cell. Recent alerts gains a This node / All nodes scope that follows the active node, a fixed eight-row preview with View all alerts, deterministic drill-downs (stack, drift tab, container logs, scan history, Fleet updates) that switch nodes race-safely, and first-class feed gaps so a silent node never reads as an all-clear. Fleet heartbeat takes the alerts card's height, orders problem nodes first, separates offline from unknown, and opens node details from any node. Configuration summary collapses to one row per domain with a per-setting Node / Account / Instance scope, reachability-gated drill-downs, stale handling, and fresher invalidation. The Stack restarts card is removed from Home, along with the stack-restarts endpoint and query it was the only consumer of. * test(e2e): scope the node-switch locator to the switcher popover * docs(dashboard): document the operational Home layout |
||
|
|
1efaa13492 |
feat(fleet): replace the Status tab with Readiness (#1952)
* refactor(stacks,security): extract status and overview builders into services
Move the bodies of `GET /api/stacks/statuses` and `GET /api/security/overview`
into `services/stackStatusesEvidence.ts` and `services/securityOverview.ts`.
Both routes keep their guards, response shapes, error handling, and timing
logs; they now delegate to the new builders.
The moved logic is verbatim apart from `req.nodeId` becoming a `nodeId`
parameter and `res.json(...)` becoming a return, so the existing route suites
cover it unchanged.
The move does restructure one thing: a builder rejection is now caught by a
`try` in the route that no longer wraps the code which throws. That propagation
was not covered by any test, so `cache-endpoints.test.ts` now pins it, including
that the real error still reaches the console rather than the 500 standing alone.
This is groundwork: the fleet readiness surface needs to read the same evidence
from a batch context rather than through one Express request per node.
Extracting first keeps that change from also being a refactor.
One accepted deviation, on the error path only. `dockerMs` and `enrichmentMs`
were route-scope bindings mutated by the cache closure, so a rejection after a
timing was recorded still let the route log that partial value. They are now
builder-scope, so a rejection logs the initial nulls instead. The HTTP
response is unchanged (500, same message); only the debug timing line differs,
and only when an unexpected throw lands between the Docker call and the
enrichment block with no stale cache entry present.
* feat(readiness): add the fleet readiness wire contract
Define the types the fleet readiness surface speaks in: the six domains,
the per-node domain cells the matrix renders, the prioritized findings
list, and the per-stack summary the tier-2 node route returns. No producer
or consumer lands in this commit; the routes and the aggregator follow.
The contract carries no scoring and no prose headlines. A finding's display
payload is a reason code the frontend maps to copy, and the two free-text
fields (finding detail and per-stack top reason) carry an explicit
redaction requirement, because the canonical contracts they pass through
can embed a materialized input path.
Also exports the reachability classifier from the blueprint preview
projection, along with the contact-source union it already returned and a
closed union for its note, so the fleet surface classifies reachability
with the same cached logic rather than writing a second classifier. The
three existing consumers are unaffected: both substitutions are in type
positions only, and 39 tests across the preview, binding-preview, and
reconciler-gate suites pass unchanged.
* feat(readiness): add the node-local per-stack readiness rollup
Adds `GET /api/stacks/readiness-summary`, the node-local half of the fleet
readiness surface. It reports the canonical per-stack update and rollback
verdicts for one node, because the Docker socket is on that node: the update
preview's remote path aborts at a hard 90s ceiling per stack, which a hub
fan-out cannot wait on.
The route adds no scoring. It selects, bounds, and reports what
`UpdateGuardService` already computes:
- one pass, aborted by a 6s deadline that starts before the compose directory
scan so the scan is charged to the same budget
- concurrency 3, and a 25-stack cap that keeps the most operationally
interesting stacks (failed checks, then updates available, then open drift
findings, then alphabetical)
- a 120s cache entry, invalidated on stack mutation through
`invalidateNodeCaches` so a fan-out does not re-run the Docker-heavy pass per
request
- the two verdicts settle independently, so a failing rollback compute cannot
erase a good update verdict; a stack whose work throws degrades to an
unavailable row rather than failing the pass
Each row carries the strongest reason behind its verdict, taken from the
canonical signals and items and passed through `redactSensitiveText`, because a
detail there can carry a materialized input path on a `node:read`-gated surface.
The served payload carries `stale`, derived from the pass timestamp rather than
from the cache outcome. The outcome is not truthful for a caller that joined
someone else's in-flight fetch: when that fetch fails and an expired entry is
served, the caller that ran it is told `stale` while every joiner is told
`inflight`, on the same old bytes. An expired entry's own timestamp names the
condition for every reader. The pass type omits the field, so a pass cannot
assert freshness it cannot know.
The contract also names the aggregate's row types (`StackReadinessRow`,
`StackUpdateReadinessRow`, `StackRollbackReadinessRow`), so the producer and its
tests refer to them rather than restating the shape at each call site.
Two registrations move together and each has its own test: the route is mounted
ahead of the `/:stackName` catch-all, and `/stacks/readiness-summary` joins
`STATIC_STACK_PATHS`. Missing the first resolves the request as a stack named
`readiness-summary`; missing the second demands exact `stack:read` on a stack of
that name, for the router gate and for the hub's own fan-out call.
`fleet-readiness-v1` joins the capability registry on both sides, following the
append-only convention there. The hub reader that gates on it lands with the
aggregator.
The route is guarded by `stack:read`, which every role holding `node:read`
already has.
* feat(fleet): aggregate fleet readiness on the hub
Add GET /api/fleet/readiness, a hub-owned aggregate over six domains
(connectivity, workloads, updates, recovery, security, control) that answers
what across the fleet needs attention before an operator acts on it.
The hub answers from its own SQLite for connectivity, stored update facts,
snapshot gaps, and Policy Sync state, and fans out two bounded node-local
reads: GET /api/readiness/evidence (3s budget) for workloads, security, and
stored recovery rows, and GET /api/stacks/readiness-summary (8s budget) for
the canonical per-stack update and rollback verdicts. Every node is wrapped
in its own try/catch into a per-node row, so one slow or unreachable node
degrades one row instead of holding the page, and a disconnected Pilot
target costs no request at all. A build without the evidence route answers
404, which is reported as a missing capability rather than as a fault.
Findings carry a stable id, an enumerated code, and a drill-down target.
Verdicts are read from the existing update and rollback readiness functions
rather than recomputed here, and nothing is scored: a check that could not
run is reported unknown or unavailable, never healthy.
The Control domain is admin-only, matching the sibling Policy Sync read. The
response omits the domain for an account that cannot see it rather than
sending it and relying on the client to hide it.
Retire GET /api/fleet/configuration and the backend normalizer that served
only it. Home's Configuration Summary keeps its own builder, and
GET /api/dashboard/configuration is unchanged.
Document the three endpoints in docs/openapi.yaml.
* feat(fleet): replace the Status tab with Readiness
Swap the Fleet tab strip's configuration inventory for a Readiness tab: a
worst-first fleet summary, a prioritized findings worklist, and a per-node
matrix over the six readiness domains. States, reason codes, and findings
all arrive decided from the hub, so the surface renders the payload and
dispatches each drill-down to the surface that already owns that
remediation.
The matrix lays out the domains the payload carries, so the Control column
is simply absent for an account the server withheld it from. Tier-2 columns
are read from each node's own cells rather than from hasCapability(), which
describes the active node and would misreport every other row.
Findings that restate an update or a rollback verdict reuse the mappings
those surfaces already publish instead of re-declaring them.
Normalize the legacy `configuration` segment to `readiness` in the route
parser so an existing deep link still resolves.
Delete the retired configuration inventory component.
* docs(fleet): document the Readiness tab
Follow the tab from Status to Readiness on the Fleet view page: the summary,
the findings worklist, the per-domain matrix, what happens when a node cannot
be reached, and the admin-only Control column. Update the tab table and the
tab-strip prose, and drop the tab list from the Overview screenshot alt text
so it stops describing a strip the screenshot no longer shows.
Replace the tab screenshot and follow the rename through Fleet Sync, the
deep-link reference, the introduction, and the API reference contents.
* test(fleet): compare readiness mock hosts by origin
The readiness fan-out tests route each mocked fetch by asking whether the
request targeted the proxy host. Decide that on the parsed origin rather
than on a prefix match against the raw URL string, which is the shape the
static analysis flags as incomplete URL handling.
No behavior change: every URL reaching a dispatcher is absolute, and each
of the four tests exercises both sides of the branch.
* fix(readiness): keep ordinary configuration and repeated causes out of findings
The aggregate reported several conditions that are not problems, and one
unreachable node could hold its row for the full verdict budget.
- A stack with no containers (not deployed) is a known state, not missing
evidence: it is no longer a Workloads concern, and its update and
rollback verdicts are left out. A stale or degraded status read never
hides a verdict.
- A fleet that never took a snapshot adds no Recovery concern; snapshots
are optional. An unreadable skip column is a check that could not run.
- Policy sync (Control) is classified only for the nodes it pushes to.
The hub's own node and Pilot-agent nodes carry no Control cell instead
of a permanent "unknown".
- When the reachability probe gets no answer, the slower verdict read to
the same remote is cancelled, so a dead remote costs the 3s probe
budget instead of the 8s verdict budget. A verdict read that failed on
its own keeps its own cause.
- When Connectivity already reports why a node could not be read, the
gated domains keep their cells but add no finding restating the same
cause. The hub's own node, whose Connectivity is always healthy, keeps
every finding.
- Rows are sorted worst first and carry their worst state.
* feat(fleet): rebuild the Readiness tab on the shared page patterns
The tab used a bespoke layout that did not match the rest of the app. It
now uses the same building blocks as the other Fleet tabs and the
Security and GitOps tables:
- The shared Fleet tab heading, then a summary strip with the worst state
as a short phrase and tiles for nodes needing attention, unverified,
healthy, and when the check last ran.
- Findings on the shared table shell with the standard filter row
(search, domain, state, verdict, node) and pagination. The verdict
filter has presets for rollback partial or not ready, and for updates
that are blocked or need review.
- A node matrix whose cells name each state in the domain's own words
("online", "2 blocked", "never scanned", "in sync", "--" where policy
sync does not apply) instead of generic state words. Clicking a problem
cell narrows the findings to it.
- The Fleet toolbar Refresh now re-runs the readiness check.
- A Security finding switches to the finding's node before opening
Security, using the same pending node-view intent the Networking
shortcut uses.
* docs(fleet): describe the rebuilt Readiness tab
Rewrite the Readiness section for the new summary strip, findings
filters, and domain-specific matrix values, refresh the screenshot, and
update the Fleet Sync references and the OpenAPI node contract (worst
state per row, Control only for policy sync targets, one finding per
unreachable node).
|
||
|
|
199013197e |
fix(e2e): read the launcher morph after the transition settles (#1971)
The hamburger bars move through a 150ms CSS transition (clamped only under Reduced motion). The test read the closed computed style immediately after an Escape, which could land inside the close transition and capture the open values; reopening then produced an identical read and the morph assertion failed intermittently depending on the suite account's motion preference. Capture the closed style while the bar is at rest before any toggle, and poll the open style instead of sampling once. The assertion still proves the bar moves between the closed and open states. |
||
|
|
38de9b6d36 |
fix(gitops): render the portfolio Rollout column through the rollout vocabulary (#1970)
The portfolio table read each row's rollout status through the runtime state lookup. Facet names shared by both facets (recovery_required, partially_rolled_out, completion_unknown, fully_deployed_health_pending) showed per-node wording and tone for a fleet-wide rollout fact, and rollout-only statuses such as canary_in_progress fell back to raw text with no tooltip. Read through ROLLOUT_STATE_LOOKUP, the fleet-wide vocabulary the application view already uses, and add a component test covering a colliding status and a rollout-only status. The test fails against the unfixed table and passes with the swap. |
||
|
|
a98ef3f26e |
chore(deps): bump the all-npm-frontend group in /frontend with 7 updates (#1962)
* chore(deps): bump the all-npm-frontend group in /frontend with 7 updates Bumps the all-npm-frontend group in /frontend with 7 updates: | Package | From | To | | --- | --- | --- | | [cronstrue](https://github.com/bradymholt/cRonstrue) | `3.26.0` | `3.27.0` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.46.0` | `1.47.0` | | [motion](https://github.com/motiondivision/motion) | `13.3.0` | `13.4.0` | | [@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` | | [jsdom](https://github.com/jsdom/jsdom) | `30.0.1` | `30.1.0` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.0` | `5.0.1` | Updates `cronstrue` from 3.26.0 to 3.27.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.26.0...v3.27.0) Updates `lucide-react` from 1.46.0 to 1.47.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.47.0/packages/lucide-react) Updates `motion` from 13.3.0 to 13.4.0 - [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md) - [Commits](https://github.com/motiondivision/motion/compare/v13.3.0...v13.4.0) 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 `jsdom` from 30.0.1 to 30.1.0 - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](https://github.com/jsdom/jsdom/compare/v30.0.1...v30.1.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) --- updated-dependencies: - dependency-name: cronstrue dependency-version: 3.27.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: lucide-react dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: motion dependency-version: 13.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@types/node" dependency-version: 26.6.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: eslint dependency-version: 10.11.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: jsdom dependency-version: 30.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: vitest dependency-version: 5.0.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend ... Signed-off-by: dependabot[bot] <support@github.com> * test(frontend): flush jsdom 30.1 deferred window blur between tests jsdom 30.1 drops a removed focused element without firing blur; the next focus() call synchronously fires a blur retargeted at the window. In the suite that stale blur lands mid-click in the following test and trips Radix's close-menu-on-window-blur handler, closing freshly opened menus (FileTree context menu, TopBar Add menu). The global afterEach now blurs the active element, cleans up, then forces a focus change on a scratch element so the stale blur fires before the next test starts. --------- 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> |
||
|
|
bb85e08b36 |
feat(gitops): add rollout lifecycle controls and rollback recovery (#1965)
* feat(gitops): capture a recovery point before Git-managed rollout applies A rollout rollback needs a recovery point that names the generation the node was running before the deploy, and Git-managed rollout applies did not create one. applyLocalUnderLock now captures the pre-deploy project (with the hub-supplied GitOps binding) before the incoming compose reaches disk, owns the recovery lifecycle from acquire through verified, and compensates from the recovery point when the apply fails. A remote target does the same over apply-local, and the request carries the binding because a leaf has no GitOps rows of its own. The binding resolves the deployed pointer for Direct mode and the ack's applied pointer for Blueprint mode: nothing writes the deploy-bound pointer for a Blueprint-managed stack. A re-capture that cannot resolve the owning application (a leaf backup, for example) carries the previous binding forward, since re-capturing does not change what the node is running. * feat(gitops): add rollout lifecycle controls and rollback coordination Adds the operator lifecycle routes for a Git-managed Blueprint rollout: pause and resume (application or one target), replan (re-derives placement and reopens review instead of invalidating an accurate one), supersede (withdraws the live authorization and reports the abandoned rollout), and rollback. Rollback restores an explicit prior application generation on one target, the failed targets, or every changed target in the frozen set. Selectable generations come from the application's rollout-generation history; targets already running the selected generation are out of scope; every target is authorized before any work starts; and the abandoned generation is superseded first so nothing can dispatch it mid-restore. Each target is restored from its own captured recovery point, in process for the local node and through the node's stack rollback route for a remote, where the node must confirm it restored the requested generation. Every exit records a terminal per-target result, and a partial failure lifts the application out of recovery rather than leaving a restore nobody drives. The application detail read and the Blueprint summary now carry the prior generations a rollback can select, plus the bound Blueprint's enabled state so the surfaces can withhold execution controls. * feat(gitops): add rollout controls to the GitOps application surfaces The GitOps application view and the Blueprint sheet's GitOps section gain a rollout controls row: pause and resume as first-row actions, with replan, supersede, and rollback behind the overflow menu and each destructive choice behind a confirmation. Rollback selects a prior generation and a target scope (one target, the failed targets, or every changed target) and states the configuration-versus-data boundary before anything runs. Controls are offered only from the projection's own statuses and gated by the permission the route requires, and execution controls are withheld while the bound Blueprint is disabled. The superseded rollout copy now covers an operator withdrawal as well as replacement. * docs(gitops): document rollout lifecycle controls and rollback Covers pausing, resuming, replanning, and superseding a rollout, the rollback generation and scope selection with its configuration-versus-data caveat, and the drift-enforce interaction that re-applies the current revision after a rollback. The GitOps portfolio page names the in-place actions on a Git-managed application, and What's New gains an entry for the lifecycle controls. * fix(gitops): sanitize logged error messages on rollout recovery paths CodeQL flagged log injection on the new rollout lifecycle and recovery handlers: a remote node's response body, a recovery row, or a candidate payload can reach the logged Error object, so the logs now record the sanitized message string instead. No behavior change. * fix(gitops): sanitize the logged target id on rollout recovery paths The target node id on these paths can come from the rollback request's scope selector, so CodeQL reads it as a user-provided value flowing into the log entry even though it is a number. Pass it through sanitizeForLog, the same barrier the other logged values use. No behavior change. * fix(gitops): hold rollout dispatch while it is paused Pause was recorded and rendered but no execution path honored it: boot-time reconstruction re-dispatched a paused rollout, and the sequential dispatch loop kept deploying targets the operator had paused. The adapter now blocks while the application is paused, re-reads the pause before every target, skips individually paused targets, and reports a hold instead of progress when nothing was dispatched because every remaining target is paused; reconstruction skips paused applications outright. A dispatch that pauses during the preflight window no longer mints or supersedes the authorization it is holding. The continuation a target resume triggers is covered by the route test: the queue is dispatched again and any still-paused target is skipped. * fix(gitops): continue the queue on target resume and refuse empty pauses A per-target resume cleared the pause but never continued the rollout, so the target it released waited for the next restart to be retried; the route now runs the same continuation as an application resume, and the adapter skips targets that are still individually paused. Pausing an application with no authorized rollout and no partial record is now refused with ROLLOUT_PAUSE_REFUSED: recording a hold on nothing makes the surface read as held when no rollout ever ran. * docs(gitops): state the per-node restore limit for rollout rollback A node keeps only the recovery generation captured immediately before its latest rollout, so an older selectable generation has nothing to restore from and is reported as a failed target. The rollback dialog and the feature page now say so instead of implying every listed generation is restorable. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |