v1.5.0-rc.15
[1.5.0-rc.15] — 2026-04-27
Fixed
- #308 — Per-row scanning chip wasn't anchored to the container being scanned (begunfx, rc.14). Backend was already broadcasting
dd:scan-started/dd:scan-completedwith{ containerId, status }payloads, but the UI's SSE service was dropping the payload on the floor — both listeners emitted bare bus events with no container reference. The per-row "Scanning" chip was therefore driven entirely by the optimistic localactionInProgressmap populated when the user clicked Scan, which (a) couldn't reflect cron-driven scheduled scans and (b) was tied to the HTTP request lifecycle instead of the actual scan lifecycle. Threaded thecontainerIdthrough the SSE service → bus → AppLayoutdd:sse-scan-started/dd:sse-scan-completedCustomEvent payloads, added a singletonuseScanLifecyclecomposable that maintains ascansInFlightset keyed by container id (with a 120s safety timeout), and refactoredscanContainerso the per-row chip is set on click and cleared by the SSE completion event (or on HTTP failure). The chip now stays correctly anchored to the row whose container is actually being scanned, regardless of whether the scan was started by a click or by the scheduler. - #308 — Scanning chip escaped its row and floated in viewport-fixed gutter space (begunfx, rc.14). The icon-column overlay chip is
position: absolute; inset: 0, which requires a positioned ancestor on the<tr>to stay pinned to the row. The repo already documents this instyle.cssand applies thetransform: translateZ(0)containing-block hack via thedd-row-updatingclass — but rc.13 deliberately decoupled scan from the lock state (scan must keep the row interactive), which removed the containing-block hack from scanning rows as a side-effect. Without a positioned ancestor the chip escaped up the layout tree and rendered at a fixed viewport position, appearing in the gutter between unrelated rows or section headers and persisting across scrolls until the scan completed. Added a siblingdd-row-scanningclass (containing block only — no opacity dimming, nopointer-events-none) and applied it fromtableRowClasswhenever a row is scanning but not locked. Locked-state still wins when a row is somehow both updating and scanning. - #317 — Lifecycle notifications silenced by
auto: false(begunfx, rc.14). A notification trigger configured withauto: false(a common Pushover setup to suppress every-detection update-available spam) was also silently losing every other lifecycle notification —update-applied,update-failed,security-alert,agent-connected,agent-disconnected. The init code wrapped all event registrations in a singleif (auto !== 'none')block. Decoupled them: auto-fire-on-detection handlers stay gated byauto, lifecycle handlers register unconditionally. A user who's configured the trigger at all now gets completion / failure / security / agent notifications regardless of howautois set. - #317 — Lifecycle notification rules silently dropped triggers without an explicit allow-list.
update-applied/update-failed/security-alert/agent-connect/agent-reconnectdispatched withallowAllWhenNoTriggers: falsewhileupdate-availableusedtrue. The asymmetry meant a notification rule with no per-rule allow-list silently disabled lifecycle notifications even thoughupdate-availableworked. Flipped the four lifecycle dispatch sites to match: empty / missing allow-lists now permit dispatch. Explicit per-rule allow-lists still win when populated. - #317 — Update button bypassed eligibility blockers, queuing requests the API would only reject one-by-one (s-b-e-n-s-o-n, rc.14). The Update button (per-row + Update-all) only gated on the legacy
bouncer === 'blocked'(security scan), even though rc.13 added 11 other eligibility blocker reasons surfaced as row pills. Clicking Update on a row pill-marked AGENT MISMATCH produced the toastNo docker trigger found for this container; clicking Update-all on a stack of TRIGGER FILTERED proxies queued every row before the API rejected each one individually. Added aseverity: 'hard' | 'soft'field toUpdateBlockerand madeupdate-eligibilitythe single source of truth: the API rejects manual updates on any hard blocker with the blocker's user-friendly message; the UI locks the Update button on hard blockers and shows a confirm-modal warning on soft blockers ("This update is currently policy-blocked: … Click Update anyway to override.").
Changed
security-scan-blockednow also fires when the current container's scan status is blocked. Previously the eligibility model only inspectedsecurity.updateScan(the candidate) whilerequest-update.tsindependently inspectedsecurity.scan(the running image). Both gates are now unified under the eligibility blocker — either beingblockedhalts a manual update with the same 409 + "Security scan is blocking this update" message. Use the existing force-update path to override.
Deprecated
dd.action.include/dd.action.exclude(and legacydd.trigger.include/dd.trigger.exclude) become hard manual-update blockers in v1.7.0. v1.5.x keeps them as soft blockers — the pill reads Trigger filtered / Trigger excluded but the manual Update button stays clickable (with a warn-and-confirm). v1.7.0 will lock the button and reject the API call when the labels filter out the matching trigger. See DEPRECATIONS.md for migration guidance.