Releases: CodesWhat/drydock
Releases · CodesWhat/drydock
v1.5.0-rc.15
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.
v1.5.0-rc.14
v1.5.0-rc.14
[1.5.0-rc.14] — 2026-04-26
Fixed
- Update-eligibility pill never rendered (rc.13 regression). The blocker pills shipped in rc.13 (
Trigger filtered,Below threshold,Maturing, etc.) silently rendered nothing for every container in every view (table rows, side panel, full-page detail, dashboard widget). The backend computed eligibility correctly and serialised it in the API response, but the UI'smapApiContainer()didn't declare or pass through theupdateEligibilityfield — TypeScript's structural typing dropped the unknown JSON property at the boundary. Fix is two-part:- UI mapper —
ApiContainerInputnow declaresupdateEligibility, andmapApiContainerpasses it through with aderiveUpdateEligibilityvalidator that rejects malformed eligibility objects (missingeligibleboolean, missingevaluatedAt) and filters out malformed individual blockers (badreasonstrings, missingmessage). One change lights up every consumer (list, side panel, full-page, dashboard). - Backend SSE enrichment — the store broadcasts raw container objects on
dd:container-added/dd:container-updated, butupdateEligibilityis computed on-demand in the list handler and never persisted. Without enrichment, live SSE patches would deliver containers with eligibility undefined, flickering the pill on every update. Newsse-container-enrichment.tshelper computes eligibility from registry triggers + active operations and attaches it to each lifecycle payload before broadcast. - Reported in #307.
- UI mapper —
- #323 — Popovers on the Containers list rendered off-screen for the last row + drifted on scroll. The "More" actions menu and the column picker both used
position: fixedwith coordinates captured from the trigger'sgetBoundingClientRect()and unconditionally opened below the trigger. When the trigger sat near the bottom of the viewport (e.g. the last container row), the menu was clipped off-screen and inaccessible. The popovers also stayed at their captured pixel coordinates while the table scrolled, drifting away from the trigger. Added a smallbuildPopoverStylehelper that measures available space below vs above and flips the popover upward when there's not enough room below; added a globalscrolllistener (capture phase, to catch internal scroll containers) that closes both popovers since their fixed coordinates can't track a moving anchor. Sibling popovers in this codebase (NotificationBell, etc.) intentionally untouched here — different UX surfaces, separate consideration.
Tests / CI
- Eligibility coverage closed end-to-end. 13 new mapper unit tests (full eligibility roundtrip including
actionHint/liftableAt/details, all 13 validUpdateBlockerReasonvalues, malformed-blocker filtering, missing-field rejection, non-arrayblockershandled as empty list). 7 new SSE enrichment tests (happy path, malformed payloads, error resilience). The 55 existingUpdateEligibilityBadgescomponent tests already covered the render gate. Manual verification: builtdrydock:devwith the fix, ran withDD_ACTION_DOCKER_LOCAL_AUTO=oninclude, confirmed 23 backendtrigger-not-includedblockers render as 23 amber "Trigger filtered" pills in the Kind column. - Popover positioning coverage. 6 new tests in
ContainersView.spec.ts: actions menu and column picker each flip up when the trigger is near viewport bottom; global scroll closes the actions menu, closes the column picker, and is a no-op when nothing is open; scroll listener is removed on unmount. Manual verification: builtdrydock:devwith the fix, scrolled the last container row to the viewport bottom, opened its More menu — the menu rendered fully within the viewport (anchored bottom: 48px from viewport top) instead of clipping off-screen.
v1.5.0-rc.13
v1.5.0-rc.13
[1.5.0-rc.13] — 2026-04-24
Added
- Update-eligibility blockers on container rows — Backend surfaces 12 structured blocker reasons (
maturity-not-reached,container-paused,no-update-detected, etc.) per container, and the Containers list renders them inline so users see why a row isn't updating without opening the detail drawer. Amber formaturity-not-reached(informational — will self-clear at the maturity threshold), red for terminal blockers. GET /update-operations/:idendpoint — Returns the current state of a specific update operation. The UI falls back to this endpoint when the terminal SSE is missed (reverse-proxy reconnect-without-replay), so update toasts and hold release still fire even when the browser never saw the final server-sent event.
Fixed
- #308 (partial) — Two of the three reporter symptoms are resolved:
- Row status during scan. Triggering a scan from the row's
...menu no longer shows "Updating" on the row.actionInProgressnow tracks the kind of action per container (update/scan/lifecycle/delete) instead of a bare id set, and the status label and row-lock predicates read from the per-kind map so a scan in progress shows "Scanning" and doesn't dim other row actions. - Batch-mode triggers on single-container scans. Providers that only listen to
emitContainerReports(batch-mode, e.g. Pushover) were silently skipped on single-container scan paths, which only emitted theemitContainerReportsingleton. Threaded{ emitBatchEvent: true }through thewatchContainercall from the single-scan handler so batch-mode triggers fire too. - Still open: the empty
updateKind.{kind,localValue,remoteValue}fields in simple-template notification email bodies reported by begunfx. The dispatch snapshot shouldn't normally loseupdateKind— needs a repro to chase the root cause.
- Row status during scan. Triggering a scan from the row's
- #317 — Two fixes around reconnect-without-replay SSE:
- Rollback actions no longer broadcast spurious container-lifecycle events (
dd:sse-container-added/-removedin quick succession) while the store is mid-recreate. - When the terminal update-operation SSE is missed, the Containers view's reconciliation pass now releases the display hold and fires the update toast from the reconciled state, matching what would've happened on a live SSE.
- Rollback actions no longer broadcast spurious container-lifecycle events (
- #318 — Kind and status columns in the Containers list now stay visible at narrow viewports. The container-query thresholds that gated column visibility were tripping too early; adjusted so the columns survive down to the minimum useful table width.
- #291 (rc.12 follow-up) — Dashboard update flow now shares the same
useOperationDisplayHoldcomposable that the Containers view uses, fixing the last two reporter symptoms that survived rc.12 on the dashboard path. (1) The updating row no longer drops to the bottom of the Recent Updates widget mid-update: the backend transiently clearsupdateDetectedAtwhile the container recreates, which was sorting the ghost row last — the shared hold now overlays the snapshottedupdateDetectedAtso sort position is stable through the whole update window. (2) A dropped terminal SSE (Synology DSM reverse-proxy style reconnect-without-replay) no longer leaves the dashboard silent: the view now reconciles holds against refreshed container data and falls through toGET /update-operations/:idthe same way the Containers view did. Both views are now driven by one hold map, one sort-snapshot overlay, and one reconciliation fallback.
Performance
- #301 (rc.11 follow-up) — Dashboard and Containers view now do dramatically less work on every SSE reconnect. Addresses the residual slow-load reports on those two pages after the rc.10/rc.11 backend fixes.
- Dashboard reconnect refresh is live-only. On
dd:sse-connectedthe dashboard now refetches only the endpoints that can go stale between frames (/containers,/containers/stats,/containers/recent-status) and TTL-guards the static ones (/server,/agents,/watchers,/registries) for 30s.dd:sse-resync-required(server-signaled state loss) still forces a full 7-endpoint fan-out — the TTL skip only applies to reconnect blips. On a flaky Synology LAN this turns a reconnect storm into a handful of requests instead of 7× that. - Dashboard stats read no longer warms Docker stats streams per container.
GET /api/v1/containers/statsaccepts a new?touch=falsequery param; the dashboard uses it so a summary read returns already-cached snapshots without spinning up a per-container Docker stats stream. The Containers view / detail panel still pass the default (touch=true) so streaming stats stay warm where they actually render. - Containers list dedup fingerprint is ~30× cheaper.
loadContainers()was recursively fingerprinting every field (includingdetails.ports/volumes/env/labelsarrays) on both the incoming and current lists to skip redundant reactive reassignment. Replaced with a flat hash of the ~13 scalar fields that actually affect row rendering. Identity-and-tag changes still trigger reassignment; deep-field-only changes that never reach the list render no longer do. - SSE lookup-map churn halved.
updateLookupMapsForContainerwas doing 4 full-map spreads ({ ...map, ...key }) per container SSE event; collapsed to 2.removeLookupMapsForContainercollapsed the same way and skips reassignment entirely when neither the id nor the alias is present.
- Dashboard reconnect refresh is live-only. On
Docs
- v1.5.0 deprecation sweep. Migrated every documentation example and test fixture off the v1.5.0-deprecated
DD_TRIGGER_*/dd.trigger.*prefixes onto canonicalDD_NOTIFICATION_*+dd.notification.*(messaging providers) andDD_ACTION_*+dd.action.*(update executors). Touched 29 files incontent/docs/current/**, the in-repo README roadmap and Recent Updates sections, CONTRIBUTING, all QA/CI/demo compose fixtures (exceptmigration-test-compose.yml, which intentionally exercises the legacy prefix), and the apps/web landing-page roadmap. The legacy prefixes still work as aliases through v1.7.0 — this sweep makes every example in the project canonical so new users stop copy-pasting deprecated forms. SeeDEPRECATIONS.md. - Registry credentials:
*_TOKEN→*_PASSWORD. The Docker Hub and DHI provider docs no longer instruct users to setDD_REGISTRY_HUB_PUBLIC_TOKEN(deprecated) — examples now useDD_REGISTRY_HUB_PUBLIC_PASSWORD, matching the canonical form every other registry provider already documents.
Tests / CI
- Dashboard helper + computed coverage — Added targeted tests for
createRealtimeRefreshScheduler'sfull-live→refreshFullfallback branch (when no full-live handler is configured) and for the WeakMap cache-hit branch ofuseDashboardComputed's name-counts memoization. Closes the last two UI coverage gaps left after rc.12.
v1.5.0-rc.12
v1.5.0-rc.12
[1.5.0-rc.12] — 2026-04-22
Fixed
- #315 — Self-update now works against private registries whose
registry.urlis stored as the v2 API base (e.g.https://ghcr.io/v2).resolveHelperImagein the Docker action trigger was building the helper image reference by concatenatingregistry.urlverbatim withname:tag, producinghttps://ghcr.io/v2/codeswhat/drydock:1.5.0— which Docker'sPOST /containers/createrejects with HTTP 400. The fix normalizes the reference to matchRegistry.getImageFullName(scheme and/v2stripped) so the self-update helper spawn uses the same image shape as the pull path. No-scheme / no-v2 registries fall back toname:tag. - #309 — Status column in the Containers list now shows its label alongside the icon at typical widths. The column was width-capped at 90px which fell below the shared
dd-cell-show-80container-query threshold (80px) once the 10px padding was subtracted — widened to 120px so the label renders whenever the column is visible. - #291 (rc.11 follow-up) — Terminal update toasts (
Updated: <name>/Update failed: <name>/Rolled back: <name>) now fire after the operation display hold ends instead of on the raw terminal SSE. The container row stays visually in "updating" for another 1.5s (OPERATION_DISPLAY_HOLD_MS) after the terminal event so it settles without flicker, but the toast was announcing the outcome before the UI reflected it. Wrapped the three terminal toast calls insetTimeout(..., OPERATION_DISPLAY_HOLD_MS)in bothContainersView.applyOperationPatchandDashboardView.handleTerminalOperationSseso the row releases first and the toast lands right after. - Dashboard fly-in animation on update actions — Three
fetchDashboardData()calls inDashboardView(single-updateonAccepted,onStale, and the update-all finalizer) were refreshing without{ background: true }, which flippedstate.loadingto true and caused the<GridLayout v-if="!loading">to unmount and remount — re-triggering grid-layout-plus's initial positioning transitions (the "fly in from the left" rebuild reporters saw after every update). Switched those three calls to the background variant so the grid stays mounted and only the underlying widget data refreshes in place. - Pre-push e2e hook timeout —
scripts/run-e2e-tests.shunconditionally restarted Colima before every run, which scaled badly once the dev host accumulated QA fixtures (VM cold-boot with 45+ attached containers exceeded the lefthook 10m timeout before cucumber started). DefaultDD_E2E_RESTART_COLIMAtoauto— skip the restart whendocker infoalready succeeds, and only force one when the engine is actually wedged. Set totrue/falseto override.
Security
- fast-xml-parser override 5.5.8 → 5.7.1 — Addresses GHSA-gh4j-gqv2-49f6 / CVE-2026-41650 (XML comment/CDATA injection via unescaped delimiters in
XMLBuilder, medium). Vulnerable range ≤ 5.5.12, patched in 5.7.0; bumped bothapp/ande2e/workspace overrides to 5.7.1 (latest). - uuid 13.0.0 → 14.0.0 — Addresses GHSA-w5hq-g745-h8pq (missing buffer bounds check in
v3/v5/v6whenbufis provided, medium). Vulnerable range ≤ 13.0.0, patched in 14.0.0. Drydock only usesv4(unaffected by the buffer path) but the scanner flags any vulnerable version in the tree. Bumped the app's direct dep and addeduuid: 14.0.0to bothapp/ande2e/overrides so transitive callers (dockerode, artillery, @azure/msal-node, @ngneat/falso) also resolve the patched version.
Performance
- Granular SSE container patch — Container list updates triggered by
dd:sse-container-added/-updated/-removednow patch the local containers array in place instead of refetching the full list over HTTP. Preserves row identity via a prototype-chain merge so Vue's reactivity system reuses the existing row DOM nodes, eliminating the post-SSE flicker and the polling refetch that the previous debounced-reload path required. shallowRefforheldOperationsmap — The operation display-hold store swapped from a deepref(Map)toshallowRef(Map)plus explicittriggerRefon mutation. Avoids Vue walking the entire map on every held-row render in large inventories.
Changed
- Expand all / Collapse all bulk toggle — Replaced the single chevron toggle in the Containers toolbar with an explicit "Expand all" / "Collapse all" button whose label reflects the current global expand state. Individual stack headers still toggle their own group; this is the bulk shortcut on top.
- Compact "Suggested" badge — The "Suggested" tag badge on container rows now renders compact (the full suggested tag moves into a tooltip on hover) so the row metadata band stays readable at narrower column widths.
Tests / CI
- Thin test hardening —
app/agent/index.test.tsgrew 5 → 11 tests. Existing coverage verified call counts but not argument correctness, same-instance registration, fire-and-forgetclient.init()behavior, skip-branch warning messages, or mixed valid/invalid registry state. Added behavioral assertions for each while keeping 100% coverage. - SSE / pending-poll / hold-release edge coverage — New tests for
useOperationDisplayHold,useContainerActions.pollPendingActionsState, andapplyDashboardContainerPatchclose the remaining uncovered branches (delete-no-op, in-flight poll guard, removed-kind patches, Object.assign merge, push-new, mapApiContainer throw, container-removed SSE listener). - QA compose fixture trims — Shrank test-fixture images so update flows finish in seconds instead of minutes, and swapped
timescaledb-ha→ non-HA to unblock scan tests that were failing against the HA variant's readiness probe. - Release pipeline fragility fixes — Three recurring CI failures in the release cut path addressed (see
a1ea1a82onfix/ci-release-fragility, renamed tofeature/v1.5-rc12).
v1.5.0-rc.11
node:unset:2: no such hash table element: node
[1.5.0-rc.11] — 2026-04-21
Added
- #299 — Inline update action in Security view. Image rows in the Security view now show an "Update" action button directly next to the vulnerability data when a newer image is available, instead of only offering navigation away to the Containers view. Single-container images open a confirmation dialog in place; multi-container images open a compact chooser popover to pick which instance to update. A secondary "View in Containers" link remains for cases where the user wants to inspect the full container state first. The
ContainerUpdateDialogcomponent is extracted as a standalone reusable piece. The Containers view now accepts a?containerIds=<csv>query parameter to pre-filter to a specific set of containers, with a dismissable filter chip in the toolbar. - SSE Last-Event-ID replay (#289) — The server now stamps every broadcast event with a monotonic
<bootId>:<counter>id and retains a 5-minute time-bounded ring buffer. Clients reconnecting with aLast-Event-IDheader receive every event they missed; if the buffer has evicted the requested id (or the server booted since), the client receives add:resync-requiredevent and the UI refetches view state. Replaces the previous best-effort reconnect that could leave container rows stuck in an "updating" state when a terminal SSE was dropped during a watcher-scan window. - Dashboard recent-updates row navigation (#311 adjacent) — Clicking a row in the dashboard "Updates Available" widget now navigates to
/containers?containerIds=<id>, focusing the Containers view on that single container. Interactive controls inside the row (Update button, Release notes link) retain their own behavior via@click.stop. - Expand/Collapse all stacks toggle (Discussion #311) — A single toggle button appears next to the Group-by-Stack icon in the Containers toolbar when Stack view is on. When every stack is collapsed the icon points down and clicking expands all; otherwise it points up and clicking collapses every stack. Individual stack headers still toggle their own group as before — this is purely a bulk shortcut on top of that.
Fixed
- #305 — Hide Pinned now hides every pinned container again, matching rc.8 behavior and the reporter's expectation when combining Hide Pinned with Has Update. #293 had carved out an exception for pinned rows with a pending update, but that conflated "declutter" with "surface actionable pins" and broke the filter for users who pin infrastructure containers (databases, edge, etc.) and want Hide Pinned to simply remove every pinned row from the list. The pin-to-wait-out-a-regression scenario from #293 is now addressed by simply unchecking Hide Pinned — predictable filter semantics over clever cross-filter logic.
- #296 (follow-up) — When
docker-socket-proxyblocksGET /info(the default —INFO=0), drydock now logs an actionable warning naming the watcher, the error, and the fix (set INFO=1 on your docker-socket-proxy config, or set DD_SERVER_NAME to override) instead of silently falling back to the container short ID. The socket-proxy setup doc and compose examples now includeINFO=1in the required environment variables. Default notification body templates no longer repeat the[server]source prefix that already appears in the title — providers that concatenate title + body (Telegram, Slack, Mattermost, Matrix, Teams, Google Chat, Rocket.Chat) are unaffected because source context is carried by the title. - #289 — Container rows no longer drop sort position during an in-flight update, and every terminal outcome (succeeded / failed / rolled-back) now fires a toast. The operation display hold captures a sort-field snapshot at hold start so the row stays pinned through the docker recreate window; the active hold extends to 10 minutes with a reconciliation pass after each container-list refresh that collapses the hold back to the 1.5s settle window for any container whose raw status no longer shows an in-progress update (safety net for missed terminal SSEs).
- #291 — Dashboard update flow now fires the same toast sequence as the Containers view: "Update started" on click (no more premature "updated" message), then "Updated / Update failed / Rolled back" on the terminal state. Wired through the same SSE-driven operation display hold that the Containers view uses.
- Concurrent dashboard update queueing — The per-row Update button in the dashboard Recent-Updates widget is no longer disabled by a global
isDashboardBulkUpdateLockedstate whenever any row is updating; each row's button now only disables for its own in-flight state, matching the Containers view and letting users stack additional updates onto the queue while one is in flight. - Dashboard updating/queued badge centering — The Updating/Queued pill on dashboard recent-updates rows now renders as an absolute-positioned row overlay using the shared
dd-row-updating/.dd-row-overlaypattern from the Containers view (badge spans horizontally and vertically centered, rest of the row dims to 30%) instead of as an inline badge next to the container name. resultChangedpreserved through env redaction —classifyContainerRuntimeEnvpreviously dropped the non-enumerableresultChangedfunction when spreading into the redacted object, which producedresultChanged is not a functionat watch time. The function is now re-attached viaObject.definePropertyafter the spread so watcher scans against redacted state continue to work.- #310 — Restored the
[server]/[agent]prefix on default notification body templates that rc.1030287c24had stripped. The original refactor reasoned that title already carried the prefix so body duplication was redundant on chat providers like Slack/Telegram; it did not account for batch email, where Gmail shows one subject ("N updates available") and a bulleted body, and each bullet needs to self-identify its watcher/host. Email correctness beats chat cosmetic duplication —DEFAULT_SIMPLE_BODY_DIGEST,DEFAULT_SIMPLE_BODY_UPDATE,UPDATE_APPLIED_SIMPLE_BODY,UPDATE_FAILED_SIMPLE_BODY, andSECURITY_ALERT_SIMPLE_BODYall lead withcontainer.notificationAgentPrefixagain.
v1.5.0-rc.10
v1.5.0-rc.10
[Unreleased]
Added
- #299 — Inline update action in Security view. Image rows in the Security view now show an "Update" action button directly next to the vulnerability data when a newer image is available, instead of only offering navigation away to the Containers view. Single-container images open a confirmation dialog in place; multi-container images open a compact chooser popover to pick which instance to update. A secondary "View in Containers" link remains for cases where the user wants to inspect the full container state first. The
ContainerUpdateDialogcomponent is extracted as a standalone reusable piece. The Containers view now accepts a?containerIds=<csv>query parameter to pre-filter to a specific set of containers, with a dismissable filter chip in the toolbar.
Fixed
- #305 — Hide Pinned now hides every pinned container again, matching rc.8 behavior and the reporter's expectation when combining Hide Pinned with Has Update. #293 had carved out an exception for pinned rows with a pending update, but that conflated "declutter" with "surface actionable pins" and broke the filter for users who pin infrastructure containers (databases, edge, etc.) and want Hide Pinned to simply remove every pinned row from the list. The pin-to-wait-out-a-regression scenario from #293 is now addressed by simply unchecking Hide Pinned — predictable filter semantics over clever cross-filter logic.
- #296 (follow-up) — When
docker-socket-proxyblocksGET /info(the default —INFO=0), drydock now logs an actionable warning naming the watcher, the error, and the fix (set INFO=1 on your docker-socket-proxy config, or set DD_SERVER_NAME to override) instead of silently falling back to the container short ID. The socket-proxy setup doc and compose examples now includeINFO=1in the required environment variables. Default notification body templates no longer repeat the[server]source prefix that already appears in the title — providers that concatenate title + body (Telegram, Slack, Mattermost, Matrix, Teams, Google Chat, Rocket.Chat) are unaffected because source context is carried by the title.
v1.5.0-rc.9
v1.5.0-rc.9 — 2026-04-17
Added
- Notification history store — New LokiJS collection (
notifications_history) records a per-(trigger, container, event-kind) result hash soonce=truededup survives process restarts and transientchanged=falsescan cycles. Replaces in-memorynotifiedHashesstate that used to disappear on restart, and lets the digest channel maintain its own send history independently of simple/batch channels (see #282). (#c0054d25, #f79d9ec6) - OIDC redirect target allowlist — Post-login redirects are now validated against the backend's endpoint allowlist (not origin matching) so OIDC flows cannot be hijacked into landing on arbitrary paths. Works alongside the rc.8 strict-origin / authorization-endpoint match. (#02c27294)
- SPA + hashed-asset cache-control — Static UI assets with hashed filenames are served with immutable long-lived cache headers, while the SPA
index.htmlfallback carries a short revalidation header so deployments are picked up on the next navigation. (#656e502a) - Label truncation with tooltips across data surfaces — Long container names, tags, and image references truncate with an ellipsis and surface the full value on hover via the shared tooltip directive (which now falls back to binding text and suppresses the native
titleattribute to avoid browser-UI duplication). (#dec4ac06, #38530312) - Bounded native container table scroll — Replaced the virtualized table implementation with a bounded native scroll region that performs predictably at 10k+ rows without the virtualization layout churn that was flagged in rc.7/rc.8 feedback. (#dfa8ecff)
Changed
- ♻️ refactor(components) — Component configuration types are now generic across the watcher / registry / trigger / authentication hierarchies, so each component carries its own typed
configurationinstead of the baseComponent<T>forcingunknowncasts at every call site. (#89abe357) - ♻️ refactor(registries) — Public-image credential-fallback logic hoisted into
BaseRegistryso every provider shares the same "private token failed → retry as public" flow, closing the per-provider drift that was surfacing in rc.8 registry logs. (#b579d810) - ♻️ refactor(triggers) —
dockeranddockercomposetriggers share a single compose bind-mount path remapper so the two code paths cannot drift on how host/container paths map across the agent boundary. (#4303d876) - 🎨 style(ui) — Updating table rows dim to 30% opacity to match the card-view treatment, keeping the visual language consistent across both layouts while an update is in-flight. (#8bcec369)
Fixed
- #282 —
batch+digestmode now sends both the immediate batch email and the scheduled morning digest for each detected update, matching the documented semantics. Since rc.7 (982b4d74), the batch path was writing a persistent'update-available'history hash and evicting the just-buffered container from the digest buffer; on every subsequent scan,handleContainerReportDigestsilently short-circuited because the sharedonce + alreadyNotifiedcheck returned true. The morning cron then fired on an empty buffer and loggedbuffer empty, nothing to send. Fix splits the digest channel off as its ownNotificationEventKind('update-available-digest') so batch-channel and digest-channel dedup are independent, removes the cross-channel eviction inhandleContainerReports, teachesseedNotificationHistoryFromStore/handleContainerUpdateAppliedEventabout both kinds, and adds a debug log on the previously-silent skip path so operators can actually see why a container didn't make it into the buffer. (#458030b7) - #293 — Hide Pinned filter no longer hides pinned containers that have a pending update. Reporter pinned
grafana/grafana:12.3.2to wait out an upstream regression and expected to see12.3.3show up as an available update, but the Containers view and the dashboard Updates Available widget both filtered it out because the classifier treats any 3+ numeric-segment semver as pinned. Filter decluttering is preserved for static pinned containers; only rows with a pending update (newTagtruthy) now surface through the filter. Updates the dashboardupdatesAvailablestat card and the update breakdown widget to count pinned-with-update rows toward the true total. (#318d97ab) - #298 — Remote-agent container updates no longer fail with
HTTP 413 Payload Too Large. The controller posted the fullContainerobject to the agent when executing adocker/dockercomposetrigger, but the agent's 256kb json body cap (introduced as v1.5.0 DoS hardening) started getting exceeded by common payloads once release-notes bodies + env + labels + image metadata grew across the RC cycle. The agent's update handler only dereferencescontainer.id(for its own store lookup) andcontainer.name(for the rollback-container guard), so the controller now posts just{ id, name }for update triggers. Notification triggers still receive the full container so template rendering works. Reporter on Synology DSM 7 sawError updating container ... (Request failed with status code 413)fromagent-client.mediavault. (#5d6c76aa) - #296 (reopen) — Controller identity detection now runs for host-based watchers (TCP to a local socket-proxy, the common Synology / Docker Compose pattern), not just pure-socket watchers. Socket-based watchers still take priority, and truly remote watchers can no longer hijack the identity because host-based detection is skipped once a name has already been populated. Reporter saw
[baf1154911ce]on rc.8 because their Synology setup reaches the daemon viadocker-socket-proxy, which made the previous!watcher.configuration.hostgate short-circuit. Workaround for users on rc.8: setDD_SERVER_NAME. (#2143804d) - Trivy DB cache race — The on-disk Trivy DB cache is now guarded against stale in-flight overrides during concurrent scans, so two scans starting within the same refresh window cannot leave each other with a half-applied database snapshot. (#89adcc11)
- Malformed
dd.tag.transformregex patterns — The regex-transform label validator now throws at config time for malformed or oversized patterns instead of silently producing broken tag transformations that surface as bogus update detections. (#0a93f113) - SSE teardown double-run — Event-stream cleanup listeners register with
{ once: true }so a client disconnect cannot run teardown twice and briefly emit duplicate metrics / log lines for the same client ID. (#bcaba6e4) - Updating badge rendering — Table rows now show a centered Updating badge instead of the tiny inline spinner that was hard to see on dense rows with long container names. (#fa558167)
- Grouped-row Update All lockout — Starting an update on one container in a stack no longer disables every other row in the same group; only the updating row itself is locked. (#6c4d10ea)
- Modal backdrop z-index —
z-indexutility classes are now registered as Tailwind@utilityrules so modal backdrops reliably cover the page instead of getting layered below dashboard widgets on certain themes. (#7bb26683) - Containers table 70vh cap — Dropped the legacy
max-height: 70vhcap on the containers table so the table fills the viewport like every other data surface, eliminating the phantom whitespace at the bottom of the page on tall displays. (#d2fceec7) - Grouped containers table 70vh cap (regressed in rc.9) — The bound-scroll fix in rc.9 reintroduced a
max-height="70vh"cap on the grouped (Stacks) table; removed so it again fills the page like flat view. The DataViewLayout already provides the page-level scroll surface — no nested scroller needed. - Table cell vertical alignment — DataTable cells default to
align-middleinstead ofalign-top. Multi-line name+image cells used to push the row taller than the actions column, leaving the actions floating in the vertical middle while every other cell stuck to the top. Centering everything keeps the row reading as a single horizontal band of metadata. - Container icon size in table view — Container icons in the grouped/flat table bumped from 20px → 32px and the icon column from 40px → 56px so they read at a glance on dense rows.
- Sidebar nav top padding — Trimmed the gap between the DRYDOCK brand and the first nav item (Dashboard) from ~14px to ~6px so the brand visually anchors to the nav grid below it instead of floating above empty space.
- **Notification bell dropdown row...
v1.5.0-rc.8
v1.5.0-rc.8
[1.5.0-rc.8] - 2026-04-13
Added
- Backend-driven update queue — Container updates are now queued server-side with per-trigger concurrency limits. UI shows Queued → Updating → Updated state progression with sequence labels (e.g. "Updating 1 of 3"). (#bacbc1c7, #5edc55a2, #7d7cfdfc)
- Watcher next-run metadata (#288) — Watcher API and Agents view now show when each watcher will next poll for updates. (#6706929e)
- Notification delivery failure audit entries (#282) — Failed notification deliveries appear in the notification bell dropdown. (#ae5309b3)
- Container action operations — Container actions (start, stop, restart, etc.) tracked with dashboard updates. (#6615ccbf)
- Identity-keyed container tracking — Containers tracked by stable identity key (agent::watcher::name) across renames/replacements. Audit events include
containerIdentityKey. Recent-status API returnsstatusesByIdentityfor precise per-container status resolution. (#8fb75070, #ea413c34) - Trigger buffer retention and capacity limits — Digest and batch-retry buffers now enforce a 5,000-entry cap and 7-day retention window, evicting the oldest entries first and pruning stale entries before each access. Prevents unbounded memory growth on long-running controllers. (#c6a9930c)
- Update operation recovery phases — New phases (
recovered-cleanup-temp,recovered-rollback,recovered-active,recovery-failed,recovery-missing-containers) distinguish operations that completed via the deferred reconciliation recovery path from the primary update flow. (#3ffe2f12)
Changed
- Auth user cache removed — Replaced TTL-based auth cache with request deduplication only, ensuring logout/session expiry in other tabs is reflected immediately. (#923e0926)
- ♻️ refactor(ui) — Container update cards now render phase-only queue status, while grouped stack headers own the frozen
X of Y donebatch progress copy for multi-container updates.
Fixed
- #296 — Notification server-name prefix no longer renders the container ID on Docker Compose setups.
getServerName()precedence is nowDD_SERVER_NAME→ detected Docker/Podman daemon host name (fromdockerApi.info().Name) →os.hostname(). Remote watchers never hijack the controller identity. (#b723369f) - #286 — Stack view column shifting fixed by collapsing all stacks into a single table. (#2f511d33)
- #283 — Duplicate server name in notification prefix and suffix suppressed. (#aaf9962d)
- #270 — Hide-pinned filter now uses computed
tagPinnedproperty instead of stale stored field. Unconditional startup repair ensures tagPrecision data is always correct. (#c7ecceef, #0949a142) - #291 — Dashboard update widget now uses the same update-start semantics as containers view (shows "Updating" toast, not "Updated"). (#c9f21a7b)
- #290 — Update-applied success events preserved across Docker container rename race. (#6b5c1f72)
- #289 — Standalone (non-queued) update state transitions restored after queue-aware changes broke them in rc.7. (#2b00c4b8)
- #287 — Custom healthcheck backward compatibility restored. The built-in
/bin/healthcheckremains the default image probe and now handles TLS backends, whilecurlis again present in the Docker image for user-defined customhealthcheck:overrides during the v1.5.x deprecation window. v1.6.0 is the final warning release, and removal is now scheduled for v1.7.0. (#414f0170) - #282 — Digest buffer evicted after batch send; watcher events awaited for handler ordering; batch send retry with delivery failure audit; agent remote report events awaited. (#982b4d74, #0594e971, #c31f78eb, #1ac7c36d)
- #276 — Dashboard update tracking keyed by container ID instead of name. (#c81e25a8)
- #256 — Pending update state scoped by stable container identity, preventing cross-contamination between same-name containers on different hosts. (#05c023fe)
- #253 — Shorthand trigger references resolved in notification rule matching; notification buffering keys stabilized; debug logging added to every silent filter path. (#ba6341b4, #d475d33c, #bb1550e4)
- #248 — API guard against duplicate container updates (409 conflict). (#110aae36)
- #217 — Vulnerability rows top-aligned in detail panels. (#431be5ea)
- Expired update operations — Executor revives expired pre-created operations instead of inserting duplicate rows. (#04e847ef)
- Static asset throttling — SPA fallback rate limiter no longer throttles static assets. (#bfe52038)
- Compound rolling tag aliases misclassified as pinned —
isTagPinnednow treats aliases likelatest-alpine,stable_arm64, anddev.buildas floating even when their suffixes contain digits. (#da613f70)
Performance
- Virtual scrolling for grouped containers table — Grouped container tables no longer render every row eagerly, keeping the DOM light on deployments with many containers. (#606d5cc0)
Security
- Axios CVE-2025-62718 — Updated axios 1.13.6 → 1.15.0. (#c4af5e4a)
- Healthcheck HTTPS probe hardening —
/bin/healthcheckno longer usespopen()with shell command interpolation to invokeopenssl. The probe now locates the openssl binary explicitly, fork/execs it with pipes, and uses poll-driven I/O with SIGPIPE handling — eliminating any shell injection surface. (#0173d7ed) - SSE log IP hashing with opt-in raw mode — SSE connect/disconnect lines and per-IP rate-limit warnings now log the internal client ID plus a salted hash of the source IP (
h:xxxxxxxx) by default. The hash salt rotates on every process start, so hashed identifiers cannot be correlated across restarts and raw addresses never touch the log. Operators troubleshooting a specific connection issue can setDD_SSE_DEBUG_LOG_IP=trueto temporarily log raw IPs. (#9e236745) - HTTP trigger proxy URL scheme validation — HTTP trigger proxy URLs must now be
http://orhttps://schemes. Invalid schemes are rejected at config-validation time and fail closed at runtime instead of silently constructing a broken proxy. (#981f7f8e) - Vulnerability CSV export escape hardening — Every CSV field (including column headers) is now quoted unconditionally, and tab/CR leading characters are escaped alongside
=+-@to fully close the CSV formula-injection surface. (#95de9f0e) - Snyk policy file — Added a repo-level
.snykfile so reviewed false-positive Snyk Code findings are silenced with a mandatory reason and expiry date alongside the code, instead of in PR comments. Initial entries cover the ephemeral session-secret generator and the UI static file sink flagged viaprocess.argv[1]taint. (#61f49606) - Supply-chain toolchain refresh — Bumped pinned Alpine edge/testing package versions for
cosign(2.4.3-r12) andtrivy(0.69.3-r2) in the Dockerfile to track upstrea...
v1.5.0-rc.7
v1.5.0-rc.7
[1.5.0-rc.7] - 2026-04-08
Added
- Multi-server notification identification (#283) — Notifications automatically include a
[server-name]prefix when agents are registered, identifying which server (controller or agent) each update comes from. Controller name configurable viaDD_SERVER_NAME(defaults toos.hostname()). Custom templates can usecontainer.notificationServerNameandcontainer.notificationAgentPrefix. (#14365870, #5880b4c8) - Infrastructure update mode —
dd.update.mode=infrastructurelabel for socket proxy containers enables helper-swap update path bypassing the socket proxy. (#0e8f620d)
Changed
- SSE debug logging — Connect/disconnect log messages now include client ID and IP address. (#12942ee4)
Security
- Vite CVE patches — Updated vite to 8.0.7 (ui) and 7.3.2 (demo) to fix CVE-2026-39363, CVE-2026-39364, CVE-2026-39365 (dev server file read vulnerabilities). (#15abb919)
v1.5.0-rc.6
v1.5.0-rc.6
[1.5.0-rc.6] - 2026-04-05
Added
- Rollback shortcut in container actions menu — Quick rollback option directly from the container row actions dropdown. (#5137b99a)
- Clear all button in notification bell — One-click dismiss of all notification bell entries. (#6785e2a4)
- Info toast when no pending update — Container update check now shows an informational toast when the container is already at its latest version. (#08864936)
- Typed notification event templates — Every event type (update-available, update-applied, update-failed, security-alert, agent-reconnect) now has a dedicated default template with type-safe variable placeholders. (#69f85c36)
- Digest-aware default notification templates — Default templates now indicate when a notification was delivered via digest accumulation. (#f7668d44)
- Structured dispatch decisions with digest routing — Trigger dispatch returns structured decision objects with digest routing warnings for debugging notification flow. (#fd82adc8)
- Agent SSE event relay — Controller now relays
update-applied,update-failed, andsecurity-alertevents from remote agents over SSE to connected UI clients. (#f497ac75) - Implicit all-triggers semantics label — Notification rule editor shows helper text when an
update-availablerule implicitly applies to all triggers. (#ba43a870)
Changed
- Responsive dashboard layout persistence — Dashboard widget bounds and layout are now breakpoint-aware, persisting separate layouts per viewport tier. (#d16a3d0f, #2deac1c3)
- Tag precision module consolidation —
TagPrecisiontype and numeric tag shape parser deduplicated intotag/precisionmodule. (#857fcb7a, #f2cc81e0) - Trigger notification internals — Extracted notification event helpers, hoisted default templates, type-safe container access in store. (#3e81f378, #42f5c7aa)
- Outlined button styling — Filled background and improved contrast for outlined action buttons across all views. (#b901295e, #898b6195)
- Axios supply chain advisory retired — Removed from README and website security documentation. (#c782d3dc)
Fixed
- #270 — Hide-pinned filter now applies to dashboard update widgets;
tagPrecisionbackfilled for existing containers on migration. (#f2e36ce4, #972f7af1) - #271 — Log sort order persists across navigation. (#dbc42186)
- #265 — Stale update detection suppressed from pre-clear watcher scans. (#06e8a4ee)
- #217 — Data rows top-aligned with scroll containment for container views. (#a97bd0a9)
- Dashboard mobile responsive layout — Recent updates widget layout fixed on mobile viewports. (#e686bdef)
- Dashboard layout hardening — Unknown widgets handled gracefully; unnecessary re-renders suppressed. (#37a33a80)
- Preference persistence on page exit — Pending dashboard layout and preference writes flushed on
pagehideandvisibilitychange. (#554b7c37, #1cffd21e) - Disabled button hover affordance — Disabled outlined buttons no longer show hover state. (#89ca53d3)
- Tag precision backfill guard —
tagPrecisionbackfill skipped when upgrading past v1.5.0. (#9deb3ba1) - Tag shape parser crash —
getNumericTagShapeguarded against undefinedtransformTagresult. (#eedc01bf) - Digest buffer stale entries — Container name canonicalized before digest buffer eviction; stale entries evicted before flush. (#547a3f15, #2776a2d6)
- Digest buffer type safety —
flushDigestBufferstore container map typed asContainer. (#d256e3af)
Performance
- Log viewer cache — Incremental newest-first cache for log viewer avoids full re-sort on new entries. (#3340b7c1)
- Vulnerability list URL memoization — Safe URL computation memoized to avoid redundant parsing. (#d9bcbe5d)
Security
- Vulnerability URL and CSV sanitization — Vulnerability URLs validated before rendering; CSV export fields sanitized against formula injection. (#c9ecf1e6)