Skip to content

✨ feat(rc): v1.5.0-rc.10 — security scan digest + notification UX rework#304

Merged
s-b-e-n-s-o-n merged 54 commits into
mainfrom
feature/v1.5-rc10
Apr 20, 2026
Merged

✨ feat(rc): v1.5.0-rc.10 — security scan digest + notification UX rework#304
s-b-e-n-s-o-n merged 54 commits into
mainfrom
feature/v1.5-rc10

Conversation

@s-b-e-n-s-o-n
Copy link
Copy Markdown
Contributor

Summary

  • Security scan digest (#300)SECURITYMODE=digest (and batch+digest) emits one severity-grouped summary per scan cycle instead of one notification per vulnerable container. New POST /api/v1/containers/scan-all scans the whole fleet server-side and emits a single security-scan-cycle-complete with stable UUID v7 cycleId. The UI Scan All button now uses the bulk endpoint so a 40-container inventory produces one email instead of forty.
  • Notification dropdown rework (#267) — Bell dropdown moved to GitHub/Linear/Slack consensus layout: per-row ✕ dismiss (hover-reveal desktop, always-on touch), a header Clear bulk action, split footer (Mark all as read / Open audit log). New --dd-zebra-stripe color-mix() token keeps alternate rows legible on every stock theme.
  • Actionable deprecation banners (#214) — All 5 deprecation banners now carry inline migration actions plus a "View migration guide" link that deep-jumps to the relevant anchor. New doc anchors: #legacy-env-vars, #legacy-labels, #legacy-trigger-prefix, #legacy-password-hashes, #curl-healthcheck-override, #oidc-http-discovery, #unversioned-api-paths.
  • Source project shortcut link (#295) — Containers render a "View project" link (GitHub/GitLab icon) next to release notes in detail panels/cards when org.opencontainers.image.source, dd.source.repo, or GHCR-derived source URL is available.
  • Watcher next-run absolute-timestamp tooltip (#288) — Next-update-check column surfaces the absolute local time on hover alongside the live relative countdown.
  • Trigger digest flush DRY refactorflushDigestBuffer / shouldHandleDigestContainerReport are now parameterized on event kind, so update-digest and security-digest share one implementation while preserving the rc.9 GMAIL: Batch and Digest Email Issues/Inconsistencies #282 dedup invariants.
  • Coverage gap closures — +610 lines of new tests across Trigger, container API, bulk-security, AgentClient, scheduler, plus mechanical securitymode: 'simple' baseline across all trigger-provider test fixtures. 100% coverage thresholds hold.

Full change list: see CHANGELOG.md [1.5.0-rc.10] section.

Test plan

…estations

Adds a user-facing "Verify Your Drydock Install" guide under content/docs/current/guides/verifying-releases/. Covers cosign keyless verification for container images (GHCR/Docker Hub/Quay), cosign verify-blob for the release tarball, and SLSA provenance verification via gh attestation verify and slsa-verifier. Uses the current release-from-tag.yml identity regex and notes the legacy release.yml regex for v1.4-and-older tags.

Wires the new page into guides/meta.json and adds a top-of-page callout from the existing Security Hardening Guide so readers verify before hardening.
Adds security-page inline update action (#299) and security scan digest (#300) to the v1.6.0 roadmap summary after ingesting both into .planning/ROADMAP.md Phases 4.5.7 and 5.9 respectively. Keeps the README table in sync with the canonical roadmap file per CLAUDE.md.
… event

Adds the notification-foundation for the security-scan-digest feature (discussion #300). Scheduled scans now emit per-container security alerts (gated by a new opt-in DD_SECURITY_SCAN_NOTIFICATIONS flag to preserve backwards compatibility) and fire a scan-cycle-complete event once the batch finishes. No trigger-side consumer yet — that lands in a follow-up that introduces digest buffering and the DRY'd flush path.

- app/store/notification-history.ts: add 'security-alert-digest' NotificationEventKind alongside 'update-available-digest'
- app/event/index.ts: new SecurityScanCycleCompleteEventPayload and emit/register ordered-handler pair
- app/security/scheduler.ts: emit emitSecurityAlert per container with critical/high findings during scheduled scans (behind the new DD_SECURITY_SCAN_NOTIFICATIONS flag), track alertCount, and emit emitSecurityScanCycleComplete in the finally block so the signal fires even on abort/throw
- app/configuration/index.ts: add scan.notifications field (default false) to SecurityConfiguration
- tests: round-trip coverage for the new event kind, emit/register coverage for the new event, gated-emission coverage in scheduler
…cycleId

Introduces a stable correlation id that threads from scheduled-scan start through every emitted security alert to the cycle-complete signal. Consumers can now group a batch of security alerts into one digest by matching the cycleId. Uses an inline RFC 9562 UUID v7 implementation (time-ordered, string-sort matches chronology) with no new npm dependency.

- app/util/uuid.ts: inline uuidv7() — 48-bit unix ms timestamp + version/variant bits + crypto random. ~25 LOC.
- app/util/uuid.test.ts: shape, monotonic ordering under a fixed clock, uniqueness across 5000 calls, version nibble, variant bits.
- app/event/index.ts: SecurityAlertEventPayload.cycleId (optional — legacy agents without it are treated as single-alert cycles); SecurityScanCycleCompleteEventPayload.cycleId is now required; scope enum widened to include 'on-demand-single' / 'on-demand-bulk' / 'agent-forwarded' for the upcoming consumers.
- app/security/scheduler.ts: generate cycleId via uuidv7() at top of runScheduledScans(); thread through runScheduledBatchDigestWorkers → scanDigestGroup → emitPerContainerSecurityAlerts; include on cycle-complete payload.
- tests: UUID v7 shape assertion on emitted alerts; same-cycle sharing across alert + cycle-complete; distinct ids across successive runs.
…ty-alert SSE

- Agent→controller SSE payload now includes optional cycleId on dd:security-alert.
- New dd:security-scan-cycle-complete event forwards scheduled-scan cycle metadata
  from agents (scannedCount, alertCount, startedAt, completedAt) with
  scope='agent-forwarded' on the controller side.
- Backward compat: controller synthesizes a cycleId + 1-alert cycle-complete when a
  legacy agent sends a dd:security-alert without cycleId, so digest buffers on the
  controller side never orphan alerts from older agents.
Implements sections 6.5 (security alert digest), 6.6 (DRY refactor), and 6.7
(SECURITYMODE config + templates) from discussion #300 plan.

6.6 — DRY refactor (behavior-preserving):
- Add DigestEventKind type, SecurityDigestEntry, SecurityDigestContext, DigestContext
- Extract formatDigestTitle + formatDigestBody pure helpers dispatching on eventKind
- Refactor flushDigestBuffer to accept { eventKind, cycleId?, cyclePayload? } options;
  update-digest path is unchanged, security-digest path uses flushSecurityDigestBuffer
- Parameterize shouldHandleDigestContainerReport on eventKind (default = update-available-digest)
- Update cron callback to pass { eventKind: 'update-available-digest' } explicitly

6.5 — Security alert digest buffering + cycle-complete handler:
- Add securityDigestBuffer: Map<cycleId, Map<containerKey, SecurityDigestEntry>>
- handleSecurityAlertEvent: digest path buffers by cycleId; no-cycleId falls through
  to immediate dispatch; simple/batch modes unchanged
- Add handleSecurityScanCycleCompleteEvent: no-op for simple mode; flushes by cycleId
  for digest-capable modes; idempotent (second call on drained cycle = no-op)
- Register/deregister via registerSecurityScanCycleComplete in init/deregisterComponent
- Extend seedNotificationHistoryFromStore to include 'security-alert-digest' when
  securitymode is digest-capable

6.7 — SECURITYMODE config + default severity-grouped digest template:
- Add securitymode, securitydigesttitle, securitydigestbody to TriggerConfiguration
- validateConfiguration joi schema: securitymode enum (simple|batch|digest|batch+digest,
  default simple); securitydigesttitle/body optional strings
- Add static normalizeSecurityMode + isSecurityDigestCapableMode helpers
- Default title: "Security scan complete: N container(s) with findings"
- Default body: severity-grouped markdown (critical → high → medium → low per section 7.4)
- renderSecurityDigestTemplate evaluates ${scan.*} template literals via Function constructor

Buffer design choice: separate securityDigestBuffer (Map<cycleId, Map<containerKey, entry>>)
rather than unifying with digestBuffer — keeps update and security paths cleanly separated
with no shared state, avoids discriminated-union complexity in the existing Container-keyed map.

28 new tests: securitymode validation, buffer accumulation, last-write-wins, overlapping
cycles, zero-alert suppression, idempotent cycle-complete, flush+drain, seed coverage,
template rendering + override, formatDigestTitle/Body dispatch.
342 total tests pass (314 pre-existing + 28 new).
- Add `emitSecurityScanCycleComplete` to `SecurityHandlerDependencies` and wire in `container.ts`
- Generate `cycleId` (UUID v7) and `startedAt` at entry of `handleScanContainer`
- Thread `cycleId` onto `emitSecurityAlert` call via updated `SecurityAlertPayload`
- Emit `emitSecurityScanCycleComplete` unconditionally in `finally` block with scope `on-demand-single`, `scannedCount: 1`, and correct `alertCount`
- Add 5 new tests: clean scan, alert+cycle correlation, throw resilience, scope/count invariants, UUID v7 shape
…aggregation

- New file `app/api/container/bulk-security.ts` — `createBulkSecurityHandlers` with `scanAll` handler
  - Accepts optional body `{ containerIds?, severity? }` with manual validation (no external dep)
  - Derives container set server-side; validates each id when containerIds provided
  - Generates UUID v7 `cycleId`, responds 202 `{ cycleId, scheduledCount }` immediately
  - Runs scans async in background with `MAX_CONCURRENT_BULK_SCANS=4` concurrency pool
  - Emits `emitSecurityAlert` (with `cycleId`) per container when critical/high threshold met
  - Emits single `emitSecurityScanCycleComplete` with scope `on-demand-bulk` in `finally`
  - Fires `broadcastScanStarted`/`broadcastScanCompleted` SSE on each iteration
  - Honors AbortSignal tied to `req.on('close')` to stop queueing on disconnect
  - Catches and logs per-scan errors; remaining containers complete normally
- New file `app/api/container/bulk-security.test.ts` — 33 tests covering validation, happy-path,
  iteration, concurrency limiting, cycleId correlation, SSE, error resilience, abort
- `app/api/container.ts` — wire `createBulkSecurityHandlers`; mount `POST /scan-all` with
  1-req/60s rate limiter using identity-aware key generator
- `app/api/openapi/paths/containers.ts` — document `/api/containers/scan-all` (202, 400, 401, 429)
…c.10

- SECURITYMODE / SECURITYDIGESTTITLE / SECURITYDIGESTBODY trigger configuration
  table rows added; notes expanded to explain that SECURITYMODE governs
  security-alert delivery independently of MODE, and that scheduled scans opt in
  via DD_SECURITY_SCAN_NOTIFICATIONS=true.
- Security guide scheduled-scans block now documents the opt-in notifications
  flag and a new "Quiet scan notifications with digest mode" subsection
  showing SECURITYMODE=digest end-to-end (scheduled cron / Scan All / cycleId
  grouping).
- CHANGELOG entry under [Unreleased] describes the scan-cycle event contract,
  the bulk POST /containers/scan-all endpoint, per-channel dedup via the new
  'security-alert-digest' NotificationEventKind, and the shared
  flushDigestBuffer refactor.
….5.0

The security scan digest feature (discussion #300) ships in v1.5.0-rc.10:
SECURITYMODE trigger config, POST /containers/scan-all bulk endpoint, UUID v7
cycleId event correlation, and per-channel dedup via security-alert-digest.
Moves the entry out of the v1.6.0 "Notifications" theme and into v1.5.0
"Observability & User-Requested Features".
…oadmap

Pulled forward into v1.5.0-rc.10: adds three bullets to the v1.5.0
roadmap block in page.tsx covering SECURITYMODE=digest, the bulk
scan-all endpoint, and UUID v7 cycleId cycle correlation.
…ndpoint

Replace the per-container HTTP fan-out in useScanProgress with a single
POST to /api/v1/containers/scan-all. Previously, a 40-container inventory
would fire 40 individual scan requests — each its own cycle — producing up
to 40 notification emails when digest mode was active. Now one POST triggers
one server-side cycle, one cycle-complete event, and one digest email.

Progress tracking shifts from return-value polling to SSE: on receiving the
202 response the composable sets total = scheduledCount and subscribes to
dd:sse-scan-completed CustomEvents (dispatched by AppLayout from the
existing dd:scan-completed SSE channel) to increment done on each container
finish. The cycleId from the 202 response is stored in a new currentCycleId
ref so downstream UI can correlate events if needed.

Rate-limit handling: a 429 from the bulk endpoint surfaces immediately as a
thrown ApiError — no retry loop. The caller is responsible for showing a
toast. AbortSignal threads through the POST fetch and the SSE-wait promise;
cancelScan() aborts both, leaving progress at whatever count was reached.

Tests updated: old per-container mock suite replaced with bulk-endpoint
mocks + SSE event dispatch helpers. New cases cover happy path (N SSE events
→ done=N), empty inventory (scheduledCount=0 completes immediately), 429
rate limit (throws, no loop), abort mid-POST, abort after partial SSE
progress, pre-aborted signal path, and singleton state sharing.
service/container.spec.ts extended with scanAllContainersApi tests covering
success, signal threading, 429, error detail parsing, and JSON parse failure.
Adds ProjectLink.vue that renders a clickable link to a container's
source project (github.com/gitlab.com/other) when the OCI source-repo
label or dd.source.repo label is present. Wired into container detail
panels (side tab, full-page overview/tabs) and the grouped list card.

Uses the sourceRepo field already surfaced by the release-notes
enrichment pipeline; no backend changes required.

Closes: #295
Add CHANGELOG entry under rc.10 Added section and append the feature
to the v1.5.0 row of the README roadmap table. Feature shipped in
d3f3ad8.
Adds formatAbsoluteTime helper and v-tooltip.top binding to the Watchers
view next-run column (table, card grid, detail panel). The primary
display remains the relative countdown ("in 14m"); the absolute local
timestamp now shows on hover, matching the existing Last-seen / Created
pattern elsewhere in the UI.

Closes: #288
Adds CHANGELOG and README entries for the dual-display tooltip
improvement on the Watchers view next-run column, shipped in 6310b60.
…214)

Rewrites the 5 deprecation banners (legacy env vars, legacy API paths,
curl healthcheck, legacy hash, OIDC HTTP discovery) to include the
concrete migration action inline and a "View migration guide" link
pointing at the relevant anchored section of /docs/deprecations.

Adds deep-link anchors to the deprecations docs page so each banner
jumps directly to the right migration instructions instead of the page
root. Defaults AnnouncementBanner's linkLabel to "View migration guide".

Closes: #214
Adds CHANGELOG and README entries for the deprecation-banner rewrite
shipped in 54fadee: inline migration paths + "View migration guide"
deep-links across all 5 banners, plus 7 new anchored sections on the
deprecations docs page.
- **Drop crowded header actions.** The pair "Mark all read" + "Clear all" in
  the dropdown header has been removed. Header now carries only the
  "Notifications" title, matching the GitHub / Linear / Slack consensus.
- **Per-row dismiss affordance.** Each entry grows an ✕ icon button that
  hover-reveals on desktop and stays visible on touch devices
  (`@media (pointer: coarse)`). Dismissed entry IDs persist to localStorage
  under `dd-bell-dismissed-ids`; the audit log is unaffected because the
  bell is a client-side read model.
- **Split footer.** "Mark all as read" and "Open audit log" share the
  footer as equal-weight actions. "Mark all as read" only shows when
  there are unread items; "Open audit log" is always visible. The old
  "View all" label is replaced with the more descriptive "Open audit log".
- **Themeable zebra stripes.** New `--dd-zebra-stripe` token derived via
  `color-mix(in srgb, var(--dd-bg-card) 92%, var(--dd-text) 8%)` adapts
  automatically to every theme. The previous `--dd-bg-inset` fallback
  was visually identical to `--dd-bg-card` on GitHub Dark, GitHub Light,
  and Ayu Light, which is what reporter flagged.

Closes discussion #267.
`test/qa-compose.demo.yml` is a local QA-only overlay that injects env
vars and healthcheck overrides to exercise deprecation banners during
manual QA. It has no production value and should not land in git.
…/footer

Follow-up to 9bccd45 based on live review. The borders separating header
/ body / footer and the divider between the two footer buttons were all
reading as distracting "lines" on the dropdown. Replaced with a
background-color contrast instead:

- Header, footer: now render on `var(--dd-bg-sidebar)`, which is the
  per-theme chrome color (noticeably darker than `--dd-bg-card` on dark
  themes, slightly off-white on light themes) — so they recede visually
  from the body rows without needing a rule line.
- Footer buttons: bumped from `dd-text-secondary` to `dd-text` with
  `hover:dd-text-primary` so the split actions are more readable against
  the new chrome bg.
- Header label: `dd-text-secondary` instead of `dd-text-muted` for the
  same reason.
- Removed: `border-bottom` on header, `border-top` on footer, and the
  1px vertical `w-px` divider between the two footer buttons.
Discussion #267 asked for a bulk Clear action alongside the per-row ✕
dismiss. The notification dropdown header now grows a small "Clear"
button in the top-right that dismisses every visible entry at once
(same local-only mechanism as the per-row ✕ — does not touch audit
history). The button hides when there are no entries to clear.
…rework

Discussion #267 entry now mentions the top-right "Clear" bulk action
added in e823d95 alongside the per-row ✕ dismiss from 9bccd45.
Mechanical update — adds securitymode: 'simple' to the configurationValid
fixtures across all trigger providers (apprise, command, docker,
googlechat, gotify, ifttt, kafka, matrix, mattermost, mqtt, ntfy,
pushover, slack, smtp, teams, telegram) so they align with the
SECURITYMODE config field introduced by the v1.5.0 security-digest
feature. No product-code behavior change.
Covers the remaining branches across the v1.5.0 security-digest and
bulk scan-all feature set:

- Trigger.test.ts (+437): security-alert-digest callback registration,
  fallback paths when container lookup or summary are missing,
  overlapping-cycle buffering, batch-failure warn logging, cycle-complete
  time fallback.
- api/container.test.ts (+96) and api/container/bulk-security.test.ts
  (+35): POST /containers/scan-all cycleId emission, filtered subsets,
  client-disconnect aborts, rate-limit path.
- agent/AgentClient.test.ts (+16): security-scan-cycle-complete SSE
  forwarding, cycleId thread-through on remote security-alert events.
- security/scheduler.test.ts (+11): scheduled-scan notification flag
  gating and zero-alert cycle emission.
Prepends security scan digest (#300), notification dropdown rework
(#267), and actionable deprecation banners (#214) so the section
reflects the v1.5.0-rc.10 scope.
…es again (#305)

Restore rc.8 behavior: Hide Pinned hides every container whose tag is a
specific/pinned version, regardless of whether an update is pending.

#293 had made Hide Pinned pass pinned rows through when they had a
`newTag`. That conflated two different needs: "declutter infra pins" and
"surface actionable pins I'm watching." Users combining Hide Pinned with
Has Update suddenly saw their pinned rows again, which is exactly the
opposite of what Hide Pinned means to them.

The pin-to-wait-out-a-regression scenario from #293 is now solved the
simpler way: uncheck Hide Pinned when you want to see pinned containers
with pending updates. Filter semantics stay predictable.

Tests in hide-pinned.spec.ts, DashboardView.spec.ts, and
useDashboardComputed.spec.ts flip their assertions to match the restored
behavior (pinned-with-update rows are now hidden again, totals + breakdown
buckets reflect only the non-pinned rows).
Close #282 follow-up (rc.9 regression).

`458030b7` split the digest channel into its own `'update-available-digest'`
NotificationEventKind so batch and digest dedup are independent. That part was
correct. The same commit also taught `seedNotificationHistoryFromStore` to seed
the new digest kind from store state at init, which is wrong: a digest-history
entry semantically means "a digest email was sent for this hash", and seeding it
from "update existed in the store at startup" is a different claim entirely.

For an operator upgrading from rc.7/rc.8 (where batch history and store state
could diverge after any transient SMTP failure), seeding locked that divergence
into the brand-new digest channel on first init. On every subsequent scan,
`shouldHandleDigestContainerReport` matched the seeded hash, silently
short-circuited, the digest buffer stayed empty, and the morning cron logged
`nothing to send` — exactly the behavior the reporter saw on rc.9.

Fix:
- Remove the `update-available-digest` entry from `kindsToSeed`. The digest
  history is now populated exclusively by a successful
  `flushUpdateDigestBuffer`, which is the only code path where "a digest email
  was sent" can be truthfully recorded.
- Keep the batch seed (`update-available`) intact — still correct, still
  prevents spurious re-batch after restart.
- Leave security-digest seeding untouched: security-alert-digest is populated
  by `flushDigestBuffer(security-alert-digest)` and not currently read as a
  dedup key, so removing it is out-of-scope for this fix.

Trade-off: the first cron after a restart or upgrade sends a catch-up digest
covering everything currently pending. This is consistent with the
"periodic summary" semantics of digest mode and is the correct failure mode
for a bounded in-memory buffer that gets re-populated each scan.

Tests:
- Flip the existing `seedNotificationHistoryFromStore seeds both ...` assertion
  so it now asserts the digest channel is NOT seeded.
- Add a regression test that proves the reporter's scenario is fixed:
  container exists in store at init, first scan cycle emits the same container
  unchanged, verify it lands in the digest buffer.
`GET /api/agents` used to do `O(agents × containers)` work per request:

- `storeContainer.getContainers()` cloned the full collection.
- `groupContainersByAgent` built a per-agent `Map<string, Container[]>`.
- For every agent row, `getAgentContainerStats` ran two `.filter()` passes
  via `getContainerStatusSummary` (running, updatesAvailable) plus a
  `new Set(containers.map(...))` for the image fingerprint count.

On the reporter's setup (3 agents, 60+ containers per #301) that's 540+
per-row predicate evaluations and multiple intermediate arrays per
request. The endpoint is called by the Dashboard, Hosts, and Agents views
so the cost compounds across the three pages begunfx called out as slow
in rc.9.

This commit replaces the entire pattern with a single pass:

- `buildStatsByAgent` pre-allocates one counter bucket per agent, then
  iterates `storeContainer.getContainersRaw({})` once. Each row
  increments its agent's `total` / `running` / `updatesAvailable` and
  adds its image fingerprint to that bucket's Set.
- `getAgentsList` maps agents to response rows using the pre-computed
  bucket — no filter fan-out, no per-agent subset arrays.
- `getContainersRaw` replaces `getContainers` because agent stats never
  need the runtime-env redaction pass and we don't want to pay for it
  here.

Removed the now-dead `getAgentContainerStats` and `groupContainersByAgent`
helpers and dropped the `getContainerStatusSummary` import from this
module (still used by `/api/container` and `agent/api/event` which aren't
hot enough to need the same refactor).

Tests:
- Swapped `getContainers` → `getContainersRaw` in the test mock; existing
  expectations unchanged because the response shape is identical.
- Added a 10-agent × 60-container regression that asserts the store is
  hit exactly once and every agent's stats are correct regardless of
  agent count — the structural guard against the `O(agents × containers)`
  pattern coming back.

Refs: #301 (rc.9 multi-endpoint slowdown hotspot 2 of 4)
GET /api/watchers and /api/watchers/:type/:name now compute per-watcher
container totals, running/stopped/updatesAvailable counts, and distinct
image fingerprints in a single pass over the raw container store and
attach them as `metadata.containers` + `metadata.images`. Reuses the
buildContainerStatsByKey helper that /api/agents now shares, so both
endpoints walk the container collection exactly once per request
instead of O(keys × containers).

ServersView and WatchersView previously fired a parallel
GET /api/containers to compute these same counts on the client — that
call is gone, along with countContainersByWatcher /
countImagesByWatcher and the WatchersView containerCounts ref. First
render of the Hosts and Watchers pages is now one round-trip per view
instead of two.

Agent-backed watchers keep their existing fallback-then-refresh path
(getWatcher RPC merges remote metadata), with the local stats fields
preserved across the remote-metadata merge so the UI sees watcher
totals regardless of whether the agent is reachable.

Backend: 25 watcher.test.ts + 11 container-summary.test.ts assertions,
including populated-store regression coverage and fingerprint fallback
(image.id → image.name → container.id) tests.
UI: ServersView + WatchersView specs updated to provide the new
metadata shape; one regression test added per view asserting the
counts flow end-to-end from payload to rendered row.
AgentsView used to iterate every connected agent in onMounted and fire
a parallel GET /api/agent/:name/log/entries?tail=50 for each one. The
entries were silently cached and only surfaced if the operator opened
that agent's detail panel and switched to the Logs tab — effectively a
precomputed view that almost nobody looks at, scaled with agent count.

Remove the eager fan-out. The existing watch() on
[agentDetailTab, selectedAgent.name] already triggers fetchAgentLogs
the first time the Logs tab is selected for a given agent, so lazy
loading is a no-op change for the detail-panel UX.

First render of the Agents page now issues one GET /api/agents and
nothing else. Called out as the third of four rc.9 Agents-page
hotspots in #301 reporter feedback.

Tests: replaced the broken "fetched only for connected agents" eager
assertion with two lazy-fetch assertions — one that logs are NOT
fetched on mount, and one that logs ARE fetched when the Logs tab is
selected in the detail panel.
)

GET /api/watchers used to do one HTTP RPC per agent-backed watcher on
every UI page load — `resolveWatcherItem` awaited
`agentClient.getWatcher(type, name)` for each registered watcher, then
Promise.all'd the bunch. For the reporter's topology (Synology LAN, 3
agent-backed watchers) that's 3 serialized WAN-ish RTTs on every
Dashboard / Hosts / Watchers render.

Replace the per-request fan-out with a push-on-change cache on the
controller:

- Extend WatcherSnapshotEventPayload to carry watcher.configuration and
  watcher.metadata. Docker.watch() now populates both from
  maskConfiguration() and getMetadata() right before emitting the
  snapshot event (lastRunAt is set inline so the payload reflects the
  cycle that just ran).
- Add watcherSnapshotCache on AgentClient. Seed from the handshake's
  existing GET /api/watchers response on connect/reconnect, refresh on
  every dd:watcher-snapshot event. updateWatcherSnapshotCache merges
  instead of overwriting so partial events don't clobber seeded values.
- resolveWatcherItem reads agentClient.getWatcherSnapshot(type, name)
  synchronously. If cache is cold (agent not yet handshaked), returns
  the local fallback — same behavior as when an agent is offline.

No protocol version bump needed — older agents that emit the pre-rc.11
payload (no watcher.configuration / watcher.metadata) fall through to
the cache's existing configuration/metadata via the merge path.

Tests:
- 6 new AgentClient cache tests covering handshake seed, SSE update,
  partial-merge semantics, and cold-cache undefined lookup.
- watcher.test.ts: getWatcher mocks replaced with getWatcherSnapshot
  (now a synchronous Map lookup), RPC-failure test repurposed to
  exercise the empty-cache fallback.
- Docker.watch.test.ts: emitWatcherSnapshot assertion extended to
  match the new configuration+metadata fields on the watcher block.

Closes the last of 4 rc.9 perf hotspots called out by begunfx.
Fixes: #301
Reproducible synthetic benchmark that models the rc.9 reporter's
Synology LAN topology (3 agents × 5 watchers × 4 containers, 30ms
simulated LAN RTT per HTTP RPC) and compares the before/after wall
clock for each of the four hotspot fixes:

- GET /api/watchers: 31ms → 0.02ms (watcher state cached on the
  controller, seeded from handshake + refreshed via dd:watcher-snapshot)
- GET /api/agents stats: 0.06ms → 0.006ms (single-pass reducer
  replaces O(agents × containers) filter fan-out)
- AgentsView mount logs fetch: 31ms → 0ms (eager per-agent prefetch
  removed; logs load lazily on Logs-tab activation)
- ServersView mount: 62ms → 31ms (redundant /api/containers call
  dropped; watcher metadata carries counts)

Run with: node scripts/bench-301-watcher-api.mjs

Not wired into CI — this is a one-off reference that can be deleted
once the perf regression risk has aged out. Check in now so the
numbers in #301 are reproducible by any maintainer.
…s filter (#299)

- Extract ContainerUpdateDialog.vue as a standalone reusable component with v-model:containerId, confirm/cancel, keyboard nav, error display, and update-kind-aware confirm message
- Security view: show "Update" action button inline on image rows with pending updates; single container opens dialog directly; multiple containers open a chooser popover to pick which instance; secondary "View in Containers" link navigates to Containers view filtered to the update containers
- Containers view: accept ?containerIds=<csv> query param to pre-filter to specific containers, with a filter chip showing count and a dismiss button
- useVulnerabilities: annotate ImageSummary with hasUpdate / containersWithUpdate cross-referencing the containers list
- ContainersViewTemplateContext: expose filterContainerIds and clearContainerIdsFilter
- Full test coverage: ContainerUpdateDialog.spec.ts (17 tests), useVulnerabilities cross-reference tests, ContainersView containerIds query tests, SecurityView update affordance tests
- CHANGELOG and docs changelog updated

Fixes: #299
…ion fresh (#299)

Extends the inline update action added in #299 so the Security view's
container update state stays in sync when watchers emit container
changes, matching the pattern used in ContainersView and the Dashboard.
…299)

Closes the gap vs. Snyk/Dependabot/Docker Scout pattern by rendering the
existing ReleaseNotesLink next to the inline Update action on the
Security view (table row, card, and detail panel). ImageSummary now
carries releaseNotes / releaseLink, pulled from the first container
with release info in containersWithUpdate.
- Ignore invalid watcher descriptors when seeding the snapshot cache from handshake (null, non-string type, non-string name)
- Skip cache updates on dd:watcher-snapshot events with non-string watcher type
- Extract mergedMetadata in resolveWatcherItem so the partial-metadata merge keeps type narrowing
- bulk-security.test.ts: inline abortHandler declaration (single-assignment → const)
- bench-301-watcher-api.mjs: split long rows.push() / console.log() calls across lines
Web components default to display:inline, so when notification-bell rows
scrolled above the fold the icon's intrinsic box could collapse and the
text column lost its reserved flex space. Pin width/height/display/flex
via inline style so the slot stays reserved regardless of layout state.
The POST /containers/scan-all handler ran Trivy scans, emitted alerts,
and broadcast completion — but never wrote scanResult back to the
container. The Security page reads from container.security, so after
a bulk cycle the UI stayed on "No vulnerability data yet" even though
real CVEs were discovered.

Mirror the single-container persistence path: call
storeContainer.updateContainer with a merged security patch and
populate the digest scan cache when a digest is available and the
scan succeeded. Persistence and cache updates are wrapped so either
failing does not abort the rest of the cycle.
Add a Fixed subsection to the 1.5.0-rc.10 entry covering the two fixes
landed today (notification bell iconify-icon collapse on scroll, bulk
security scan persistence to container.security.scan) and bump the
section date to 2026-04-19.
@s-b-e-n-s-o-n s-b-e-n-s-o-n merged commit 37fe406 into main Apr 20, 2026
24 checks passed
@s-b-e-n-s-o-n s-b-e-n-s-o-n deleted the feature/v1.5-rc10 branch April 20, 2026 00:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants