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 styling — Replaced the per-row bottom border with alternating row backgrounds (
var(--dd-bg-card)/var(--dd-bg-inset)), matching the zebra-striped pattern every other data table in the app uses.
Performance
- #301 —
GET /api/containerspreloads all active update operations in a single indexed scan and builds per-container id/newId/name lookup maps, replacing the rc.8 per-container 3-scan fan-out. Dashboard / Watchers / Servers / Container Logs views all hit this endpoint, so the synthetic per-attach time drops from ~5–25 ms to ~0.02–0.04 ms on large inventories. (#76023d15) - Container list projection via Proxy — The container list response projects each row through a
Proxythat lazily computes expensive derived fields and drops the redundantcountquery, shaving a second lap through the store per request. (#ac567d76)
Security
- GHSA-xq3m-2v4x-88gg (critical) — Bumped
protobufjs7.5.4 → 7.5.5 inapp/ande2e/lockfiles to close the prototype-chain arbitrary-code-execution advisory. (#2b473f23) - GHSA-r4q5-vmmm-2653 (medium) — Bumped
follow-redirectsto 1.16.0 inapp/lockfile to close the custom-auth-header cross-domain leak advisory. (#bbda0cd8) - Hook command grammar validator — User-supplied pre-update / post-update hook commands are now validated against a restricted shell-safe grammar at config time (no
$(...), backticks, redirections, or chaining metacharacters) so a hook label cannot escape into arbitrary shell execution. (#e29d5711) - OIDC authorization endpoint strict match — The OIDC flow now requires an exact match against the discovered authorization endpoint and no longer falls back to a broad origin match, closing a redirect-URL substitution surface. (#e81a4924)
- OIDC token redaction in error logs — Error log lines from the OIDC pipeline redact bearer / id / refresh tokens so a malformed provider response cannot leak credentials into operator logs. (#2053a5d8)
- Rate-limit key derivation — Unauthenticated rate-limit buckets now key on
socket.remoteAddressin preference torequest.ip, eliminating theX-Forwarded-Forspoof-ability for IP-based buckets when reverse proxies are untrusted. (#cb36affa) - CORS origin required when enabled — Enabling CORS now requires
DD_SERVER_CORS_ORIGINto be set explicitly; the previous wildcard default silently allowed any origin when the env var was forgotten. (#ba82d917) - Snyk policy refresh —
.snykscope narrowed to the two genuinely-reviewed Code false-positives; non-enforced rules dropped so the policy file stays a real exception ledger rather than a catch-all suppression list. (#8a2360a9) - Trivy pin refresh — Bumped the Dockerfile
apk addpin for Trivy from0.69.3-r2to0.70.0-r0to track Alpine edge/testing. The previous pin started failing builds once edge advanced past it.