* fix: recognize clean one-shot completions in health gate and drift
Treat exit 0 with restart policy no/absent as successful completion so
init and migration jobs no longer fail post-update observation or show as
service-missing, while long-running restart policies still fail closed.
* fix: ignore residual health on clean one-shots and honor deploy.restart_policy
Completed exit-0 jobs with no-restart intent no longer fail the health gate on leftover starting/unhealthy state, and Drift treats deploy.restart_policy with Compose precedence so any/on-failure services are not mistaken for one-shots.
* fix: require explicit Compose restart no for one-shot recognition
Docker inspect reports restart no for both intentional jobs and bare services that omit restart, so Health Gate and Drift now require declared restart:""no"" (or deploy.restart_policy condition none) and load Compose intent once per gate.
* feat: add service-scoped Compose update and restore
Allow updating or rebuilding one declared Compose service on multi-service
stacks without recreating siblings, with recovery snapshots, health-gate
observation, and prune holds for rollback images. Full-stack update paths
and single-service UX stay unchanged.
* fix: sanitize service-scoped update log messages for CodeQL
* fix: address service-scoped update audit findings B-01 through B-07
* fix: complete service-scoped update audit metadata and surfaces
* test: wrap Updates readiness tests for deploy-feedback context
* fix: keep service recovery reachable without Deploy Progress
Make failed service-gate recovery discoverable when Deploy Progress is
disabled or dismissed, suppress stale image-scan notification side
effects, normalize ComposeService line endings, and add focused
regression coverage.
* fix: resurface ContainersHealth density and expand on multi-service stacks
Service grouping hid the summary strip and Compact/Detailed/Expand controls that still applied to multi-container stacks.
* fix(drift): reconcile the drift ledger on deploy and timestamp its history
The drift ledger (persisted history + activity timeline) only advanced
when someone clicked re-check on a stack's Drift tab, so the history could
sit indefinitely out of sync with the live status: a stack reading
"drifted" live while its history still said "resolved". Two corrections:
- Deploy and update reconcile the ledger against the just-deployed runtime
(the rollback route re-deploys through deployStack, so it is covered),
resolving what the change fixed and recording what it left.
- Every authoritative reconcile stamps the dossier last-checked time, and
the Drift tab labels its history "checked {time}" so a stale finding
reads as history, not a claim about the live status above it.
Adds the last_drift_check_at column and tests across the ledger reconcile
stamp, reconcileStack, the deploy hook, and the panel.
* fix(drift): stamp last-checked inside the ledger transaction
Move the dossier last-checked stamp into the same transaction as the
finding insert/resolve, so the "checked {time}" the Drift tab shows can
never persist without the ledger update it describes. The stamp still runs
on a no-op authoritative check (a transaction that only stamps), keeping
the history "as of" honest. Adds a test that a failed deploy does not
reconcile the ledger.
* feat(stacks): persist a drift ledger with temporal source-change detection
Build on the read-only compose-vs-runtime drift check so a stack's drift is
remembered over time, not just shown at a glance.
- Record a deploy baseline: on a successful deploy, update, or rollback, store
the deployed compose file's source and rendered-model hashes on the stack so
the Drift tab can tell whether the file has changed since the last deploy.
- Surface temporal drift in the Drift tab: "matches last deploy", "source
changed since last deploy" (distinguishing a model change from a
formatting-only edit), or "no deploy baseline yet".
- Persist findings into a drift ledger: a re-check reconciles the current
findings, recording newly detected ones and resolving cleared ones, and shows
a short drift history under the findings. The drift report read stays
side-effect-free; only an explicit re-check (and a deploy) writes the ledger.
- Write drift detected/resolved events to the stack Activity timeline so the
provenance sits alongside deploys and restarts.
Node-local and available on the Community tier. Reconciliation is skipped when
a check is not authoritative (Docker unreachable or a compose parse error) so an
open finding is never falsely cleared.
* fix(stacks): record the drift baseline for every deploy path and harden the ledger
Address review feedback on the drift ledger:
- Record the deploy baseline in ComposeService.deployStack/updateStack instead of
only the manual route, so bulk, Git-source, App Store, scheduler, and webhook
deploys all capture source/rendered hashes. Reconciliation stays on the explicit
re-check.
- Store no rendered baseline when the local parser cannot model the compose (for
example a file over the parse cap) rather than a sentinel that would make a later
real change read as unchanged.
- Let temporal-overlay failures surface as a 500 instead of being hidden behind a
neutral "no baseline"; only the compose read stays best-effort.
- Omit the temporal card entirely when a report (for example from an older remote
node) carries no temporal data, instead of showing a misleading "no baseline".
- Keep drift_detected / drift_resolved history-only by excluding them from the
routable-category whitelist, so they are never offered as a channel route that
would never fire.
- Use a JSON separator for the finding identity key so the source file is plain
text (no embedded control byte).
* fix(stacks): sanitize logged errors in the drift report handlers
The drift report and re-check handlers logged the caught error object
raw alongside the stack name, which a code scan flagged as a
log-injection vector: a crafted stack name surfacing inside an error
message or stack could forge log lines. Route the error through the log
sanitizer so control characters are stripped before writing. Render it
with util.inspect first so the stack trace, cause chain, and underlying
error codes are preserved for debugging.