Skip to content

Merge upstream/main into fork (v1.5.0-rc.12)#44

Draft
Copilot wants to merge 1479 commits into
mainfrom
copilot/rebase-main-branch-on-upstream
Draft

Merge upstream/main into fork (v1.5.0-rc.12)#44
Copilot wants to merge 1479 commits into
mainfrom
copilot/rebase-main-branch-on-upstream

Conversation

Copy link
Copy Markdown

Copilot AI commented Apr 23, 2026

Rebases the fork onto upstream main at a00798b (v1.5.0-rc.12), resolving all conflicts while preserving fork-specific features.

Conflict resolution strategy

Dropped from fork (upstream superseded):

  • requireinclude trigger config field and all associated schema, API-layer enforcement, and tests — upstream removed this concept entirely
  • mustTrigger API-layer guard — upstream evolved trigger dispatch differently
  • requireinclude preservation logic in mapComponentToItem (app/api/component.ts)
  • Inline TLS/cert wiring in Docker.initWatcher — moved to docker-remote-auth.ts upstream

Kept from fork (fork-specific features):

  • Compose label-driven trigger creation: ensureComposeTriggersFromStore, appendTriggerId, removeTriggerId, getComposeFilePathFromLabels, compose-native path resolution, all compose label constants
  • Digest-pinning in docker-compose trigger: splitDigestReference, normalizeImageWithoutDigest, buildUpdatedComposeImage, digest-stripped image matching
  • sanitizeComponentName / CONTAINER_TRIGGER_DEFAULT_NAME in app/registry/index.ts
  • getRegistryProviderName / getRegistryDisplayName in ui/src/services/registry.ts
  • [Unreleased] CHANGELOG section (compose features, MAU registry, TrueForge registry fix)

Merged (both sides contributed):

  • Docker.ts image-matching: upstream's priority-ordered explicit checks + fork's digest-stripped comparison tiers
  • Docker.ts watch loop: fork's catch block retained alongside upstream's new finally cleanup (endDigestCachePollCycleForRegistries, emitWatcherStop)
  • Docker.test.ts / Dockercompose.test.ts: fork's compose/digest tests appended to upstream's new test suites
  • Docker.ts exports: fork's appendTriggerId, removeTriggerId, getComposeFilePathFromLabels added to upstream's expanded testable-export list

- Remove duplicate TagPrecision type from docker-helpers
- Import from canonical tag/precision module in docker-image-details-orchestration
- Add unit tests for getNumericTagShape and classifyTagPrecision
- Add toSafeExternalUrl to reject non-http(s) protocols in vuln links
- Prefix formula-leading CSV fields with apostrophe to prevent spreadsheet injection
Replace computed reverse with a shallowRef + append-aware cache that reuses
the existing array when new entries are appended, avoiding a full copy on
every streamed log line.
…ssary re-renders

- Filter unknown widget IDs in createDefaultLayoutForBreakpoint instead of crashing
- Memoize responsive layouts so unchanged breakpoints preserve referential identity
…n, and hide-pinned filter

- Add downloadVulnReport tests for CSV/JSON format selection and early-return guards
- Test buildDashboardContainerMetrics updateContainers option isolation
- Add unit tests for matchesHidePinnedFilter and filterContainersByHidePinned
…re defaults

Update configurationValid in all trigger provider tests to match the new
digest-aware simpletitle/simplebody default templates.
- Test warnIfDigestRoutingIsSuppressed early return for unrecognised dispatch reasons
- Test handleContainerReportDigest skip when changed is false
- Test flushDigestBuffer using current store container when update is still available
- Test flushDigestBuffer preserving concurrent buffer replacement during revalidation
- Test renderBatchTitle with empty containers array
- Test handleContainerUpdateFailedEvent and handleSecurityAlertEvent when container is not in store
- Test AgentClient ignoring update-applied with empty string data
…for defensive guards

- Test toSafeExternalUrl, cancelIdleCallback flush, SBOM stringify failure, migration sanitize
- Export createResponsiveLayoutsMemo for direct unit testing of layout memo
- Add v8 ignore annotations for SSR guards, ?? fallbacks, and short-circuit equality chains
The outlined variant used dd-text-muted and dd-border which were nearly
invisible in dark themes (2:1 contrast, border darker than background).
Bump to dd-text-secondary and dd-border-strong so buttons are findable
without hovering.
… views

- Add dd-bg-button utility (maps to dd-border-strong token) for a surface
  that's visible against every background in every theme
- Change outlined variant from border+muted-text to filled bg+readable text
- Add variant="outlined" to all action buttons in ContainerFullPageTabContent
  which were missing it (defaulting to invisible muted variant)
- Add variant="outlined" to "Update Now" button in both side panel and
  full-page views for consistency
Only run the backfill migration when crossing from pre-1.5.0 to 1.5.0+,
not on every startup after 1.5.0.
…ion module

Export getNumericTagShapeFromTransformedTag from tag/precision and re-export
from tag-candidates, removing the duplicate implementation.
… access

- Add TriggerContainer and TriggerNotificationContainer types to eliminate
  unsafe Container casts for notification event payloads
- Use getContainersRaw() instead of getContainers() for trigger dispatch
  and digest flush so env vars aren't redacted before template rendering
- Fix AgentDisconnectedNotificationEvent kind to exclude agent-reconnect
- Add tests for raw store resolution and update-failed batch title template
…onsiveLayouts

- Move layout hydration, persistence, breakpoint management, and memo into
  a dedicated composable for separation of concerns
- Cache widget breakpoint bounds in DashboardView computed instead of
  calling getWidgetBoundsForBreakpoint per GridItem prop binding
- Add delegation test verifying useDashboardWidgetOrder imports the new module
Compute toSafeExternalUrl once per vulnerability in a computed property
instead of calling it per v-if/v-bind in the template loop.
…and document log viewer cache

- Test non-update-available rules show correct trigger summary and help text
- Test rollback menu action sets activeDetailTab and selects container
- Add explanatory comment for newest-first log viewer append cache
…precision dedup

The tag/precision module now imports transform from tag/index, so the
auto-mock needs an explicit transform implementation that returns the
tag unchanged when no formula is provided.
…nd website

The axios npm compromise (March 2026) is resolved; remove the banner and
dedicated advisory page.
- Add disabledVariantClasses map so disabled buttons keep their fill/text
  but drop hover effects that imply interactivity
- Add disabled:cursor-not-allowed and disabled:opacity-60 globally
…date

Add staleMessage option to executeContainerActionState so update and
force-update actions show "Already up to date: <name>" instead of
silently succeeding when the container has no pending update.
…obile

- Responsive column config hides icon column and labels on small screens
- Inline container icon in cell for mobile, tighter padding via scoped CSS
- Responsive header padding with sm: breakpoint
- Test visibilitychange and pagehide flush pending layout persistence
- Assert Rollback Latest button is disabled when no backups are available
s-b-e-n-s-o-n and others added 26 commits April 16, 2026 18:51
v1.5.0 has not shipped yet, so the remaining RC work belongs in the 1.5 line rather
than a post-1.5.0 point release. The scope previously slated for v1.5.1 (scanner
decoupling, Grype provider, scanner asset lifecycle, dashboard custom grid, containers
table redesign) now moves forward into v1.6.0 alongside notifications and release intel.

- README roadmap table: remove `v1.5.1` row and merge its items into `v1.6.0`
- apps/web/app/page.tsx: flip v1.5.0 status to `next` (still unreleased), flip the
  former v1.5.1 card to v1.6.0 and mark `planned`, drop the separate v1.6.0 card
…enabled

CORS previously defaulted to `origin: '*'` and merely logged a deprecation warning when
enabled without an explicit `DD_SERVER_CORS_ORIGIN`. A wildcard CORS origin on an
authenticated API is a real security risk: any browser on any site could read responses
on behalf of a logged-in user. Warn-and-continue is not acceptable for this — the
default has to be fail-closed.

- `getServerConfiguration`: joi schema now requires `cors.origin` when `cors.enabled`
  is true; the `'*'` default is gone.
- `configureCors`: throws immediately when CORS is enabled without a trimmed
  `DD_SERVER_CORS_ORIGIN`, stopping startup before the middleware attaches.
- Keep the existing "CORS is enabled" warning so operators still see a reminder about
  the surface they just opened up.
- Tests updated: the former "warns about wildcard default" case now asserts the init
  error; two new cases cover the explicit wildcard and explicit trusted-origin paths.
  Configuration schema test pins the new required-origin behaviour.
…thenticated rate-limit keys

Express populates `req.ip` from `X-Forwarded-For` when `trust proxy` is configured —
and will populate it from the raw header even when it isn't, depending on middleware
order. That means unauthenticated clients could spoof their IP through the header and
sidestep per-IP rate limits simply by incrementing the header between requests, so
bursty abuse from a single host looked like traffic from thousands of different IPs.

- `getIpRateLimitKey` now reads `request.socket.remoteAddress` first and only falls
  back to `request.ip` when the socket address is absent. Socket-level address comes
  straight from the TCP peer and cannot be spoofed without actual network control.
- `IdentityAwareRateLimitRequestLike` picks up an optional `socket` field so callers
  and tests continue to typecheck.
- New test asserts two requests with different `ip` values but the same socket address
  collapse to the same rate-limit key.
…road origin fallback

`isAllowedAuthorizationRedirect` used to let a redirect through if the target `origin`
matched any discovered allowed origin — the full endpoint URL never had to match the
authorization server's `authorization_endpoint` metadata. An attacker who could serve
a different path under the same origin (shared-tenant IdPs, path confusion on proxies)
could steer the redirect to a controlled endpoint.

- `isAllowedAuthorizationRedirect` now requires the normalised endpoint to be present
  in `strictEndpoints`. Empty-strict-endpoints = deny, rather than the old "fall back
  to origin allowlist" behaviour.
- `OidcConfiguration` interface extracted and `Authentication<OidcConfiguration>`
  generic applied so `validateConfiguration`/`maskConfiguration` carry the concrete
  type instead of `any`.
- Tests: reject-without-endpoint-metadata path added; `redirect should reject
  authorization redirects when authorization endpoint metadata is missing` replaces the
  old "allow discovery-origin redirects…" case. `initAuthentication should tolerate
  startup discovery failure…` now supplies the authorization endpoint so the test
  actually exercises recovery after the stricter check.
- Unused `expectDiscoveryFallbackRedirectPayload` helper removed.
…safe grammar

Lifecycle hooks ran via `execFile(sh, '-c', command)`, so any user-configured command
had access to the full POSIX shell — including `&&`, `||`, `;`, `|`, backticks,
`\$(...)`, redirections, glob expansion, and arbitrary quoting tricks. Even with
DD_HOOKS_ENABLED gating the feature off by default, the docs nudge operators to enable
it on trusted hosts, and someone crafting container labels (or modifying a running
compose file) could smuggle arbitrary commands once hooks were on.

- Add a small whitelist-parser: commands must consist of tokens made of safe
  characters (`[A-Za-z0-9_./:@%+=,-]`), single- or double-quoted strings (no unescaped
  newlines, no backticks in double quotes, no command substitution), and simple
  `\$VAR` / `\${VAR}` expansions. Anything else — metacharacters, subshells,
  redirection, unterminated quotes — is rejected with `INVALID_HOOK_COMMAND_MESSAGE`
  before the command ever reaches `execFile`.
- Runner runs validation before the DD_HOOKS_ENABLED gate so misconfigured hooks get
  a clear "unsupported shell syntax" failure instead of silently skipping.
- New unit tests exercise the accepted grammar (plain args, quoted strings, variable
  expansions, nested braces, path arguments) and the rejected cases (operators,
  substitutions, unterminated quotes, null bytes, newlines inside quotes).
…ross components

All component base classes (`Component`, `Registry`, `BaseRegistry`, `Authentication`,
`Trigger`, `Watcher`, `Agent`) now accept a `TConfiguration extends BaseConfiguration`
generic so subclasses carry their concrete config interface all the way through
`configuration`, `validateConfiguration`, `maskConfiguration`, and the register/init
paths. Previously every subclass fell back to `Record<string, unknown>` / `any`, which
meant a typo in a config field (`authurl` vs `authUrl`, `clientemail` vs `clientEmail`)
compiled cleanly and silently broke at runtime.

- `Component<TConfiguration>` threaded through the registry. `register()` and
  `validateConfiguration()` return the concrete configuration type.
- `Registry<TConfiguration>` and `BaseRegistry<TConfiguration>` re-parameterised; every
  provider (ACR, Codeberg, Custom, DHI, DOCR, ECR, Forgejo, GAR, GCR, GHCR, Gitea,
  Gitlab, Hub, IBMCR, Mau, Quay, `shared/SelfHostedBasic`) declares its own
  `{Provider}RegistryConfiguration` interface and extends the base with it.
- `Authentication<TConfiguration>` + `Basic<BasicConfiguration>` + existing
  `Oidc<OidcConfiguration>` (already committed) now consistent.
- `Trigger<TConfiguration>` + all trigger providers (Apprise, Command, Discord, Docker,
  Dockercompose, Googlechat, Gotify, Http, Ifttt, Kafka, Matrix, Mattermost, MQTT,
  Ntfy, Pushover, Rocketchat, Slack, SMTP, Teams, Telegram) declare concrete config
  interfaces. `ContainerUpdateExecutor` tightens `getConfiguration()` to
  `{ dryrun?: boolean }`. `SelfUpdateConfiguration` drops its open index signature.
- `Watcher<TConfiguration>` + Docker watcher typed.
- `Agent<AgentConfiguration>` typed; `registerAgents` casts the incoming config to
  `AgentConfiguration` at the seam.
- Consumer edges (`api/docker-trigger.ts`, `updates/request-update.ts`) switch from
  `configuration?: Record<string, unknown>` to `configuration?: object` so callers
  can narrow at the use site without forcing every provider to widen.
- `store/index.test.ts` mocks `./notification-history`; `Trigger.test.ts` inherits the
  notification-history mock + mock-clear for the digest revalidation test.
- `Component.typecheck.ts` + new `Agent.typecheck.ts` pin the generic contracts — ts
  compiles clean across the app now.

No runtime behaviour changes — this is all types. The payoff is that a future typo in
any provider configuration turns into a compile error at the subclass declaration
instead of a silent runtime null deref.
…odesWhat#298)

When the controller forwards a docker / dockercompose update trigger to
a remote agent, `AgentClient.runRemoteTrigger` was posting the full
`Container` object as the axios request body. The agent's express json
parser was capped at 256kb in `a45d533b` (v1.5.0 DoS hardening), and
common container payloads have grown across the v1.5 RC cycle
(release-notes body, env, labels, image metadata, digest watch config,
observability fields). For a `linuxserver/calibre:latest` container
with enrichment, the payload exceeds 256kb and express returns HTTP
413 before `triggerApi.runTrigger` ever runs — axios on the controller
surfaces it as `Request failed with status code 413`, which the
reporter saw as broken container updates on rc.8.

For `docker` / `dockercompose` update triggers the agent's handler
(`app/api/trigger.ts:172-186`) only dereferences two fields from the
posted body:

- `container.id` — for `storeContainer.getContainer(id)` (the agent
  uses its own stored record from there on).
- `container.name` — for the `Trigger.isRollbackContainer` guard.

Everything else is dead weight. The fix posts `{ id, name }` for the
update-trigger types via the existing `REMOTE_UPDATE_TRIGGER_TYPES`
set. Notification triggers (smtp, slack, etc.) still receive the full
container because their handlers render templates against it.

- AgentClient.runRemoteTrigger: construct the posted payload
  conditionally — minimal for update triggers, full container
  otherwise. Inline comment documents why.
- Regression tests in AgentClient.test.ts:
  - docker update trigger with 300kb release-notes body posts only
    `{id, name}` (would previously have 413'd).
  - dockercompose update trigger behaves the same.
  - smtp notification trigger keeps posting the full container.

Batch path (`runRemoteTriggerBatch`) is only used by notification
batching, so it is intentionally left untouched — batched updates
don't flow through that endpoint.

Fixes: CodesWhat#298
…e Pinned (CodesWhat#293)

Hide Pinned used to hide every container classified as pinned
(`tagPinned === true`), regardless of whether that container had a
pending update. Reporter pinned `grafana/grafana:12.3.2` to wait out
an upstream regression and expected to see `12.3.3` appear as an
available update. Instead, the Containers view Updates Available
grouping, the dashboard Updates Available widget, and the dashboard
`updatesAvailable` stat card all filtered Grafana out because
`classifyTagPrecision` treats any 3+ numeric-segment semver as
`specific` / `tagPinned = true`. Filter was defeating exactly the
workflow a temporary pin is for.

The classifier is not wrong — `12.3.2` is a specific version. The
filter semantics were. Hide Pinned is a decluttering filter; a pinned
container with an actionable update is the opposite of clutter.

`matchesHidePinnedFilter` now keeps a pinned container visible when
its `newTag` is set. Static pinned rows (no pending update) are still
hidden. Effect:

- ContainersView (useContainerFilters): Grafana-style rows surface.
- DashboardView Updates Available widget + stat card + breakdown
  (useDashboardComputed's `updateContainers`): pinned rows with
  updates count toward the true total and render in the list.

Also addresses the rc.7-only flip-flop reporter observed ("appeared
again this morning"). That flip-flop was a separate stored-
`tagPrecision` backfill race — rc.8's switch to computed `tagPinned`
(`c7ecceef`) already eliminated it, but kept the filter semantics
permanent. Without this change, upgrading the reporter to rc.8+ would
have pinned Grafana out of the list forever.

Tests:
- ui/tests/utils/hide-pinned.spec.ts: added "returns true for pinned
  containers with a pending update (CodesWhat#293)" and extended the filter
  fixture with a pinned-with-update row that must survive the filter.
- ui/tests/views/dashboard/useDashboardComputed.spec.ts: flipped the
  previously-passing "hides pinned containers from dashboard update
  widgets" expectation — now asserts both rows render, totalUpdates
  is 2, and the minor-kind breakdown bucket is 1.
- ui/tests/views/DashboardView.spec.ts: equivalent flip on the
  end-to-end mount — Updates Available stat reads 2 and the Recent
  Updates widget contains both floating and pinned rows.

290 UI tests across the touched suites pass.

Fixes: CodesWhat#293
…nds both emails

In batch+digest mode, commit 982b4d7 (rc.7) added an unconditional digest-
buffer eviction after every successful batch send AND let the batch path
record 'update-available' in the persistent notification history. On the next
scan, `handleContainerReportDigest` gated the buffer add on
`shouldHandleSimpleContainerReport`, which consults the same
'update-available' history entry — so the gate always returned false and the
container was permanently locked out of the digest buffer. The morning
digest cron then fired on an empty buffer and logged "nothing to send",
which is exactly what begunfx reported on CodesWhat#282 with the 2026-04-17 log.

Split the digest channel off as its own notification history kind:

- Add `'update-available-digest'` to `NotificationEventKind` so batch-channel
  and digest-channel dedup are tracked independently in `notifications_history`.
- New helper `shouldHandleDigestContainerReport` that mirrors the simple
  variant but checks the new kind; `handleContainerReportDigest` now uses it
  and emits a debug log on the silent-skip path (previously invisible — the
  reporter's log showed pushover logging its `once=true` skip but gmail
  emitting nothing at all).
- `flushDigestBuffer` records `'update-available-digest'` after a successful
  flush; the batch path keeps recording `'update-available'`; neither stomps
  on the other.
- Remove the digest-buffer eviction in `handleContainerReports` — the
  digest channel now dedups against its own history instead of relying on a
  destructive cross-channel eviction, so the doc promise at
  `content/docs/.../triggers/index.mdx:105` ("both modes simultaneously —
  immediate batch emails plus a scheduled digest summary") holds.
- `seedNotificationHistoryFromStore` seeds both kinds for digest-capable
  modes, preserving the "fresh restart doesn't spam" property per channel.
- `handleContainerUpdateAppliedEvent` clears both kinds so a newly-detected
  update after an applied one re-fires on both channels.

Test coverage: invert the now-incorrect "batch should drain digest buffer"
test; add cross-cycle tests (re-buffer when hash changes, skip when hash
unchanged), per-channel seed tests, `recordNotification` attribution test,
and the silent-skip debug log guard. All 327 Trigger/history tests pass.

Fixes: CodesWhat#282
- app/package-lock.json: protobufjs 7.5.4 → 7.5.5 (transitive via dockerode)
- e2e/package-lock.json: protobufjs 7.5.4 → 7.5.5 (transitive via artillery)

GHSA-xq3m-2v4x-88gg (critical): arbitrary code execution in protobufjs
prototype chain. Patched in 7.5.5. Closes Dependabot alerts CodesWhat#104 and CodesWhat#105,
and unblocks the qlty pre-push gate on feature/v1.5-rc9.

Dependabot alert CodesWhat#103 (follow-redirects) is already resolved in the
lockfile (1.16.0 ≥ 1.16.0 patched) — it will auto-close on the next
Dependabot rescan after push.
…llback, and trim agent debug payload

Three low-severity code smells surfaced during post-fix verification:

- AgentClient.runRemoteTrigger debug log was serializing the full container
  even when the posted payload was trimmed to {id, name} for update
  triggers. Moved the log after payload computation and log the actual
  payload instead, so debug output matches what went over the wire.

- buildPreloadedActiveOperationLookup returned undefined for empty stores,
  silently falling back to the per-row path. Added a comment explaining
  the fallback is intentional — the per-row path handles empty stores
  correctly on its own, and this keeps the rare case on a known-good path
  instead of growing a second empty-map branch.

- buildAutoTriggerErrorSignature keys by watcher + error, not by container.
  That looked like an aliasing risk (same-named containers on the same
  watcher could share a signature) but is deliberate: a burst of identical
  errors from one system-level condition (SMTP down, agent disconnected)
  should produce a single warn log, not one per container. Added a comment
  recording the intent so future readers don't regress it.

No behavioral changes. All 456 Trigger / AgentClient / list handler tests
still pass.
The Unreleased section had only three entries (CodesWhat#293, CodesWhat#298, CodesWhat#296) while
the branch carries ~40 commits since v1.5.0-rc.8. Backfilled the
remaining entries grouped by category:

- Added: notification history store (persistent once-dedup), OIDC
  redirect allowlist, SPA + hashed-asset cache-control, label truncation
  with tooltip directive, bounded native container table scroll.
- Changed: generic component configuration types, registry public-image
  credential fallback hoisted into BaseRegistry, compose bind-mount
  remap shared between docker triggers, dimmed updating row styling.
- Fixed: CodesWhat#282 (batch+digest digest channel split), CodesWhat#293, CodesWhat#298, CodesWhat#296
  (already present), Trivy DB cache race, malformed tag-transform regex,
  SSE teardown double-run, centered updating badge, stack Update All
  row lockout, z-index utility registration, containers table 70vh cap.
- Performance: CodesWhat#301 (preloaded active update operations) and container
  list Proxy projection.
- Security: GHSA-xq3m-2v4x-88gg (protobufjs), GHSA-r4q5-vmmm-2653
  (follow-redirects), hook command grammar validator, OIDC strict
  authorization-endpoint match, OIDC token redaction, rate-limit key
  via socket.remoteAddress, CORS explicit DD_SERVER_CORS_ORIGIN, Snyk
  policy scope narrowing.

README Recent Updates list refreshed with the six highest-impact new
items pulled forward from the Unreleased changelog, keeping the
existing rc.8/rc.7 highlights below.
… HookRunner, Trigger, helpers

New / expanded tests to bring every touched file to 100% lines/branches/
statements/functions:

- list.test.ts: covers the new createProjectionView descriptor override
  path (writable handling for non-configurable source descriptors) plus
  the preload-vs-per-row selection in buildContainerListResponse.
- BaseRegistry.test.ts: covers the hoisted
  authenticateBearerFromAuthUrlWithPublicFallback — providerLabel log
  wording, non-Error rethrow, and the public-credential fallback path.
- notification-history.test.ts: parametric test for the new
  update-available-digest kind (independent of update-available).
- update-operation.test.ts: edge cases on listActiveOperations /
  ACTIVE_STATUSES snapshotting.
- helpers.test.ts: exercises the expanded mock request/response helpers
  so the helper surface itself is covered.
- HookRunner.test.ts: covers the simplified parser after dead-branch
  removal (unterminated single-quote, missing token after operator,
  variable-reference-without-$ entry point).
- Trigger.test.ts: adds coverage for the simplified digest-skip log
  format plus hasAlreadyNotifiedForResult short-circuit when no
  containerId is available.
- compose-file-sync.test.ts + ComposePathBindMounts.test.ts (new):
  coverage for the bind-mount remapper that now ships as a shared
  helper between docker / dockercompose triggers.

Source simplifications were needed to make the coverage complete:

- HookRunner.ts: removed three unreachable guard clauses (the $ prefix
  is already checked by the caller, embedded newlines inside
  single-quoted segments are already rejected by the grammar, and the
  end-of-string / no-consumed-token branches cannot be reached given
  the loop's preceding whitespace skip).
- Trigger.ts: removed redundant container.updateAvailable &&
  once===true pre-checks from the alreadyBuffered computation (both
  conditions are already required by shouldHandleDigestContainerReport
  before the log path is reached) and tightened the log formatting to
  `once === true` rather than `once ?? false`.
- list.ts: dropped the empty-overrides fast-exit in createProjectionView
  (all call sites pass at least one override) and made the property-
  descriptor writable determination robust when the source property is
  non-configurable with a non-writable descriptor.

317 test files, 6851 tests pass; coverage gate holds at 100%.
…d snooze test fixture

- ContainersGroupedViews.vue: add max-height="70vh" on the grouped-view
  DataTable. The grouped (Stacks) view virtualization is off, so the
  table needs its own scroll bound — without it, the full rendered list
  pushes the outer page into a secondary scroll, and the sticky header
  detaches. The flat containers view does NOT get this cap (covered by
  d2fceec); the two layouts intentionally scroll differently.
- ContainersGroupedViews.spec.ts: updated "disables virtualization and
  bounds native table scrolling" expectation to match.
- useContainerPolicy.spec.ts: the snoozed-policy fixture was hard-coded
  to 2026-04-14, which became a past date on today's runs and broke the
  "snoozed: true" assertion. Hoisted to a 2099-04-14 constant so the
  snooze stays in the future indefinitely.
…ine edge

Alpine's edge/testing repo advanced past 0.69.3-r2 — the exact-version
pin started failing the base image build with
`trivy-0.70.0-r0: breaks: world[trivy=0.69.3-r2]`. Bumped the pin to
0.70.0-r0 so the base image can assemble again. Both versions are safe
per our Trivy supply chain advisory (we're not affected regardless of
version).
…gistration

scripts/start-drydock.sh registers two triggers in the E2E instance as
of commit 264a1b9 (the dashboard Playwright race-fix): docker.local
with AUTO=false, plus the existing mock.example. The old fixtures still
asserted "length 1" on /api/triggers and the legacy "No docker trigger
found" 404 on /api/containers/{id}/{stop,start,restart}, so the e2e
gate was failing on pre-existing drift, not on any new code.

- api-trigger.feature: collapse the all-triggers scenario to check
  minimum length 2 and valid JSON shape. The specific-trigger scenario
  below (unchanged) is the authoritative contract check for mock.example.
- api-v14.feature: flip the lifecycle assertions from 404/error-body to
  200/"Container {stopped,started,restarted} successfully". The round-
  trip leaves the container back in running state, so follow-on
  scenarios that expect hub_nginx_120 alive stay green.

Both feature files now include a comment explaining why the e2e
instance carries docker.local, so a future reader doesn't revert the
change thinking the legacy fixtures were canonical.
…events

On every PR synchronize, both `push` (ref=refs/heads/feature/...) and
`pull_request` (ref=refs/pull/<n>/merge) fire the ci-verify workflow.
The old concurrency group keyed on `github.ref`, which differs between
the two events, so both ran to completion in parallel — ~10 duplicate
jobs per push on the rc.9 PR alone.

Key the concurrency group on the source branch name instead
(`head_ref` on PRs, `ref_name` everywhere else). Both events now land
in the same slot, and the existing `cancel-in-progress: true` cancels
whichever started first. Main-branch pushes and merge_group events
continue to run against their own ref names unchanged.
…once

Keeping `feature/**` on `push` meant every PR synchronize fired CI twice
— once as the push event and once as the pull_request event. The prior
commit (fd14ad0) papered over this via concurrency cancellation, but
the duplicate run still started and consumed runner seconds before
getting killed.

Feature-branch CI now runs only via the `pull_request` trigger: one
run per PR event, no cancellation race, no duplicated job graph. Direct
pushes to `main` and `release/**` continue to trigger CI via `push`
for post-merge and release workflows where there is no PR.

Solo feature-branch pushes without an open PR will not run CI in GitHub
Actions. The lefthook pre-push gate (biome → qlty → coverage → build →
e2e → playwright → zizmor) already runs locally on every push, so the
developer-feedback loop is preserved.

The concurrency group retains the `head_ref || ref_name` key so rapid
successive pushes to the same branch still cancel the in-flight run.
- Agent.typecheck.ts: prefix bare member access with `void` so
  CodeQL stops flagging the `// @ts-expect-error` probe as a
  no-effect expression while still triggering the type error the
  test asserts on.
- HookRunner.isAllowedHookCommand: drop the dead `consumedToken`
  local — its only assignment was unread; the token-required guard
  is the existing `nextIndex === undefined || nextIndex === index`
  return on line above.
- Trigger.test.ts: add `expect(triggerBatchSpy).toHaveBeenCalled()`
  assertions in the two batch+digest cross-cycle tests so the
  spies have an observed effect (CodeQL Useless-variable findings).

All 341 tests in the impacted files still pass; biome clean.
QA'd locally against test/qa-compose.yml and tightened a handful of UI
density/alignment papercuts that surfaced in the v1.5-rc.9 review.

- ContainersGroupedViews: drop the rc.9-reintroduced
  `max-height="70vh"` cap on the grouped table. DataViewLayout already
  hosts a page-level scroller; the nested cap was producing a second
  scrollbar and phantom whitespace under the rows. Flat view never had
  the cap; this restores parity.
- DataTable: default cell vertical alignment switches from `align-top`
  to `align-middle`. Multi-line name+image cells were pushing rows
  taller than the actions column, leaving the action buttons floating
  mid-row while every other cell hugged the top. Centering everything
  reads as a unified horizontal band of metadata.
- ContainersGroupedViews + ContainersView: container icon size 20→32 +
  icon column width 40px→56px so the icons actually register on dense
  rows.
- AppLayout: sidebar nav `py-3` → `pt-1 pb-3`. Brand-to-first-nav-item
  gap drops from ~14px to ~6px so the DRYDOCK brand anchors to the nav
  grid instead of floating above dead space.
- NotificationBell: replace per-row `border-bottom` with alternating
  `var(--dd-bg-card)` / `var(--dd-bg-inset)` backgrounds, matching the
  zebra-striped pattern every other DataTable in the app uses.

Tests updated to match (DataTable.spec.ts, ContainersGroupedViews.spec.ts).
All affected unit tests pass; verified visually in qa-compose.
✨ feat(rc): v1.5.0-rc.9 — batch+digest dedup split, CodesWhat#301 list perf, socket-proxy identity, remote-agent 413 fix
…ork (CodesWhat#304)

## Summary

- **Security scan digest
([CodesWhat#300](CodesWhat#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
([CodesWhat#267](CodesWhat#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
([CodesWhat#214](CodesWhat#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
([CodesWhat#295](CodesWhat#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
([CodesWhat#288](CodesWhat#288 —
Next-update-check column surfaces the absolute local time on hover
alongside the live relative countdown.
- **Trigger digest flush DRY refactor** — `flushDigestBuffer` /
`shouldHandleDigestContainerReport` are now parameterized on event kind,
so update-digest and security-digest share one implementation while
preserving the rc.9 CodesWhat#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

- [x] Vitest app suite — 100% coverage thresholds
- [x] Vitest ui suite — 100% coverage thresholds
- [x] Biome lint — 1 non-fatal warning (`useConst` in
`bulk-security.test.ts:641` — biome classifies let→const as unsafe
auto-fix; fix queued as follow-up)
- [x] qlty gate — clean
- [x] Cucumber e2e — 48 scenarios / 388 steps pass
- [x] Docker image build — succeeds
- [ ] Reporter verification on rc.10 once tagged
- [ ] CodesWhat#267 — confirm Clear + per-row ✕ + zebra striping read well on
their theme
- [ ] CodesWhat#300 — confirm digest email groups vulnerabilities by severity and
"Scan All" sends one email
- [ ] CodesWhat#214 — confirm deprecation banner links deep-jump correctly to
migration sections
  - [ ] CodesWhat#288 — confirm next-run tooltip shows absolute time
- [ ] CodesWhat#295 — confirm source-project link renders for watched containers
with OCI source labels

---------

Co-authored-by: superuserjr <80784472+turbodaemon@users.noreply.github.com>
…dashboard toasts, SSE replay, dashboard UX polish, CodesWhat#311 expand-all stacks (CodesWhat#312)

## Summary

Release candidate 11. Mix of bug fixes (update-flow correctness for CodesWhat#289
/ CodesWhat#291 / CodesWhat#293-regression-guard) and UX polish on the dashboard and
containers views. Full entries in `CHANGELOG.md`.

### Highlights

- **CodesWhat#289** — container rows no longer drop sort position mid-update;
every terminal state (succeeded / failed / rolled-back) now fires a
toast; missed terminal SSEs are self-healing via a reconciliation pass
after each list refresh.
- **CodesWhat#291** — dashboard update flow now emits the same toast sequence as
the Containers view (*"Update started"* → *"Updated / Update failed /
Rolled back"*).
- **SSE Last-Event-ID replay protocol** — monotonic `<bootId>:<counter>`
ids + 5-minute ring buffer + `dd:resync-required` fallback on
boot-mismatch or buffer-evicted, replacing the best-effort reconnect.
- **Concurrent dashboard update queueing** — per-row Update button is no
longer gated by a global bulk-lock.
- **Centered Updating/Queued badge on dashboard** — ported the
`dd-row-updating` / `.dd-row-overlay` pattern from the Containers view.
- **Dashboard row click opens container** — navigates to
`/containers?containerIds=<id>`.
- **Discussion CodesWhat#311** — Expand/Collapse all stacks toggle in the
Containers toolbar (icon flips based on state).
- **`resultChanged` preserved through env redaction** — fixes
`resultChanged is not a function` in watcher scans.

## Test plan

- [x] UI coverage 100/100/100/100 (166 test files, 2664 tests)
- [x] App coverage 100/100/100/100 (320 test files)
- [x] Pre-push gauntlet green locally: biome / qlty / coverage / build /
e2e / e2e-playwright
- [x] Manual QA on the QA stack for CodesWhat#289 (row stability + terminal
toasts), CodesWhat#291 (dashboard toasts), concurrent queueing, centered badge,
row-click navigation, expand/collapse-all
- [ ] CI-verify on this PR green
- [ ] QA by @begunfx across CodesWhat#289 / CodesWhat#291 / CodesWhat#272 / CodesWhat#311 once RC is cut

---------

Co-authored-by: superuserjr <80784472+turbodaemon@users.noreply.github.com>
…hat#309), perf/UX polish, CVE bumps, CI hardening

Release branch merge. See CHANGELOG [1.5.0-rc.12] for full details.
…-branch-on-upstream

# Conflicts:
#	.github/workflows/ci.yml
#	CHANGELOG.md
#	app/api/component.test.ts
#	app/api/component.ts
#	app/api/container.test.ts
#	app/api/trigger.test.ts
#	app/api/trigger.ts
#	app/registry/index.ts
#	app/triggers/providers/Trigger.test.ts
#	app/triggers/providers/Trigger.ts
#	app/triggers/providers/command/Command.test.ts
#	app/triggers/providers/dockercompose/Dockercompose.test.ts
#	app/triggers/providers/dockercompose/Dockercompose.ts
#	app/triggers/providers/gotify/Gotify.test.ts
#	app/triggers/providers/ifttt/Ifttt.test.ts
#	app/triggers/providers/kafka/Kafka.test.ts
#	app/triggers/providers/mqtt/Mqtt.test.ts
#	app/triggers/providers/ntfy/Ntfy.test.ts
#	app/triggers/providers/pushover/Pushover.test.ts
#	app/triggers/providers/slack/Slack.test.ts
#	app/triggers/providers/smtp/Smtp.test.ts
#	app/triggers/providers/telegram/Telegram.test.ts
#	app/watchers/providers/docker/Docker.test.ts
#	app/watchers/providers/docker/Docker.ts
#	ui/src/services/registry.ts

Co-authored-by: Crow-Control <7613738+Crow-Control@users.noreply.github.com>
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

4 participants