diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49002a65..f07cec35 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,8 +12,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [1.4.0] — 2026-02-28
+### Breaking Changes
+
+- **MQTT `HASS_ATTRIBUTES` default changed from `full` to `short`** — This changes Home Assistant entity payloads by default, excluding large SBOM documents, scan vulnerabilities, details, and labels. To retain the previous payload behavior, set `DD_TRIGGER_MQTT_{name}_HASS_ATTRIBUTES=full` explicitly.
+
### Added
+- **Audit log for container state changes** — External container lifecycle events (start, stop, restart via Portainer or CLI) now generate `container-update` audit entries with the new status, so the audit log reflects all state changes, not just Drydock-initiated actions. ([#120](https://github.com/CodesWhat/drydock/discussions/120))
+- **mTLS client certificate support** — Registry providers now accept `CLIENTCERT` and `CLIENTKEY` options for mutual TLS authentication with private registries that require client certificates.
+
#### Backend / Core
- **Container recent-status API** — `GET /api/containers/recent-status` returns pre-computed update status (`updated`/`pending`/`failed`) per container, replacing the client-side audit log scan and reducing dashboard fetch payload size.
@@ -55,6 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Security vulnerability overview endpoint** — New `GET /api/containers/security/vulnerabilities` returns pre-aggregated vulnerability data grouped by image with severity summaries, so the Security view no longer needs to load all containers.
- **MQTT attribute filtering for Home Assistant** — MQTT trigger supports attribute-based filtering for Home Assistant integration, allowing selective publishing based on container attributes.
- **Docker Compose post_start env validation** — Docker Compose trigger validates environment variables in `post_start` hooks before execution, preventing runtime errors from missing or invalid env var references.
+- **MQTT HASS entity_picture from container icons** — When Home Assistant HASS discovery is enabled, `entity_picture` is now automatically resolved from the container's `dd.display.icon` label. Icons with `sh:`, `hl:`, or `si:` prefixes map to jsDelivr CDN URLs for selfhst, homarr-labs, and simple-icons respectively. Direct HTTP/HTTPS URLs pass through unchanged. ([#138](https://github.com/CodesWhat/drydock/issues/138))
+- **`dd.display.picture` container label** — New label to override the MQTT HASS `entity_picture` URL directly. Takes precedence over icon-derived pictures when set to an HTTP/HTTPS URL.
#### UI / Dashboard
@@ -88,6 +97,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Rollback confirmation dialog** — Container rollback actions now require explicit confirmation through a danger-severity dialog before restoring from backup.
- **Update confirmation dialog** — Container update actions now require explicit confirmation through a dialog before triggering an update.
- **SHA-1 hash deprecation banner** — Dashboard shows a dismissible deprecation banner when legacy SHA-1 password hashes are detected, prompting migration to argon2id.
+- **Config tab URL deep-linking** — Config view tab selection syncs to the URL query parameter, enabling shareable direct links to specific config tabs.
### Changed
@@ -119,9 +129,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **6 color themes** — Replaced original Drydock theme with popular editor palettes: One Dark, GitHub, Dracula, Catppuccin, Gruvbox, and Ayu. Each with dark and light variants.
- **Argon2id password hashing** — Basic auth now uses argon2id (OWASP recommended) via Node.js built-in `crypto.argon2Sync()` instead of scrypt for password hashing. Default parameters: 64 MiB memory, 3 passes, parallelism 4.
- **PUT `/api/settings` deprecated** — `PUT /api/settings` now returns RFC 9745 `Deprecation` and RFC 8594 `Sunset` headers. Use `PATCH /api/settings` for partial updates. PUT alias removal targeted for v1.5.0.
+- **Basic auth argon2id PHC compatibility** — Basic authentication now accepts PHC-format argon2id hashes (`$argon2id$v=19$m=...,t=...,p=...$salt$hash`) in addition to the existing Drydock `argon2id$memory$passes$parallelism$salt$hash` format. Hash-generation guidance now recommends the standard `argon2` CLI command first, with Node.js as a secondary option.
+- **Borderless UI redesign** — Removed borders from all views, config tabs, detail panels, and shared data components for a cleaner visual appearance.
+- **Dashboard version column alignment** — Version column in the dashboard updates table is now left-aligned for better readability.
+- **Detail panel expand button redesigned** — Full-page expand button in the detail panel now uses a frame-corners icon instead of the previous maximize icon.
+- **Sidebar active indicator removed** — Removed the blue active indicator bar from sidebar navigation items for a cleaner look.
### Fixed
+- **Log level setting had no effect** — `DD_LOG_LEVEL=debug` was correctly parsed but debug messages were silently dropped because pino's multistream destinations defaulted to `info` level. Stream destinations now inherit the configured log level. ([#134](https://github.com/CodesWhat/drydock/issues/134))
+- **Server feature flags not loaded after login** — Feature flags (`containeractions`, `delete`) were permanently stuck as disabled when authentication was required, because the pre-login bootstrap fetch failure marked the flags as "loaded" and never retried. Now failed fetches allow automatic retry after login. ([#120](https://github.com/CodesWhat/drydock/discussions/120))
+- **Compose trigger silently skips containers** — Multiple failure paths in the compose trigger were logged at `debug` level, making it nearly impossible to diagnose why a trigger reports success but containers don't update. Key diagnostic messages (compose file mismatch, label inspect failure, no containers matched) promoted to `warn` level, and the "already up to date" message now includes container names. ([#84](https://github.com/CodesWhat/drydock/discussions/84))
+- **Fallback icon cached permanently** — The Docker placeholder icon was served with `immutable` cache headers, causing browsers to cache it permanently even after the real provider icon becomes available. Fallback responses now use `no-store`.
+- **Basic auth upgrade compatibility restored** — Basic auth now accepts legacy v1.3.9 Basic auth hashes (`{SHA}`, `$apr1$`/`$1$`, `crypt`, and plain fallback) to preserve smooth upgrades. Legacy formats remain deprecated and continue showing a migration banner, with removal still planned for v1.6.0.
+- **Compose trigger rejects lowercase env var keys** — Configuration keys like `COMPOSEFILEONCE`, `DIGESTPINNING`, and `RECONCILIATIONMODE` were lowercased by the env parser but the Joi schema expected camelCase. Schema now maps lowercase keys to their camelCase equivalents. ([#120](https://github.com/CodesWhat/drydock/discussions/120))
+- **Compose trigger strips docker.io prefix** — When a compose file uses an explicit `docker.io/` registry prefix, compose mutations now preserve it instead of stripping it to a bare library path. ([#120](https://github.com/CodesWhat/drydock/discussions/120))
+- **Compose trigger fails when FILE points to directory** — `DD_TRIGGER_DOCKERCOMPOSE_{name}_FILE` now accepts directories, automatically probing for `compose.yaml`, `compose.yml`, `docker-compose.yaml`, or `docker-compose.yml` inside the directory. ([#84](https://github.com/CodesWhat/drydock/discussions/84))
+- **Container healthcheck fails with TLS backend** — The Dockerfile healthcheck now detects `DD_SERVER_TLS_ENABLED=true` and switches to `curl --insecure https://` for self-signed certificates. Also skips the healthcheck entirely when `DD_SERVER_ENABLED=false`. ([#120](https://github.com/CodesWhat/drydock/discussions/120))
+- **Agent CAFILE ignored without CERTFILE** — The agent subsystem now loads the CA certificate from `CAFILE` even when `CERTFILE` is not provided, fixing TLS verification for agents behind reverse proxies with custom CA chains.
+- **Service worker accepts cross-origin postMessage** — The demo service worker now validates `postMessage` origins against the current host, preventing potential cross-origin message injection.
+
- **Action buttons disable and show spinner during in-progress actions** — Container action buttons (Stop, Start, Restart, Update, Delete) now show a disabled state with a spinner while the action runs in the background, providing clear visual feedback. The confirm dialog closes immediately on accept instead of blocking the UI.
- **Command palette clears stale filter on navigation** — Navigating to a container via Ctrl+K search now clears the active `filterKind`, preventing stale filter state from hiding the navigated container.
- **Manual update button works with compose triggers** — The update container endpoint now searches for both `docker` and `dockercompose` trigger types, matching the existing preview endpoint behavior. Previously, users with only a compose trigger saw "No docker trigger found for this container".
@@ -186,6 +213,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Backup retention on failed updates** — Backup entries are now pruned on the failure path, not just after successful updates, preventing indefinite accumulation of stale backups.
- **Backup pruning with undefined maxCount** — `pruneOldBackups()` no longer deletes all backups when `maxCount` is `undefined` (e.g. when `DD_BACKUPCOUNT` is not configured). Now correctly no-ops on invalid or non-finite values.
- **Auto-rollback audit fromVersion accuracy** — Rollback audit entries now correctly record `fromVersion` as the failing new image tag (via `updateKind.remoteValue`) instead of the pre-update old tag.
+- **HASS entity_picture URL broken after logo rename** — MQTT HASS discovery payload referenced a renamed logo file (`drydock.png` instead of `whale-logo.png`), causing missing entity pictures in Home Assistant. ([#138](https://github.com/CodesWhat/drydock/issues/138))
+- **Watcher crashes on containers with empty names** — Docker watcher's same-name deduplication filter threw errors when containers had empty or missing names. Now skips deduplication for unnamed containers.
+- **Container names not reconciled after external recreate** — Containers recreated externally (via Portainer or `docker compose up`) retained stale names in the store until the next full poll cycle. Now reconciles container names immediately on detection.
+- **Nested icon prefixes fail proxy request** — Icon proxy rejected icons with doubled prefixes like `mdi:mdi-docker`. Now normalizes nested prefixes before proxying.
+- **Colon-separated icon prefixes rejected** — `dd.display.icon` labels using colon separators (e.g., `sh:nextcloud`) were rejected by the API validation pattern. Validation now accepts colon-prefixed icon identifiers.
+- **Bouncer-blocked state missing from container details** — Container detail views didn't reflect bouncer-blocked status. Now correctly wires the blocked state into detail panel display.
### Security
@@ -227,6 +260,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Identity-aware rate limit keying** — Opt-in `DD_SERVER_RATELIMIT_IDENTITYKEYING=true` keys authenticated route rate limits by session/username instead of IP, preventing collisions for multiple users behind shared proxies. Unauthenticated routes remain IP-keyed. Disabled by default.
- **Reactive server feature flags in UI** — Container action buttons (update, rollback, scan, triggers) are now gated by server-side feature flags via a `useServerFeatures` composable. When features like `DD_SERVER_FEATURE_CONTAINERACTIONS` are disabled, buttons show a disabled state with tooltip explaining why instead of silently failing at runtime.
- **Compose trigger hardening** — Auto compose file detection from container labels (`com.docker.compose.project.config_files`) with Docker inspect fallback, pre-commit `docker compose config --quiet` validation before writes, compose file reconciliation (warn/block modes for runtime vs compose image drift), optional digest pinning (`DIGESTPINNING` trigger config), compose-file-once batch mode for multi-service stacks, multi-file compose chain awareness with deterministic writable target selection, compose metadata in update preview API, and compose file path display in container detail UI.
+- **Unsupported hash formats fail closed** — Basic auth now rejects unsupported hash formats instead of falling through to plaintext comparison, preventing accidental plaintext password acceptance.
### Performance
@@ -706,7 +740,6 @@ Remaining upstream-only changes (not ported — not applicable to drydock):
| Fix codeberg tests | Covered by drydock's own tests |
| Update changelog | Upstream-specific |
-[Unreleased]: https://github.com/CodesWhat/drydock/compare/v1.4.0...HEAD
[1.4.0]: https://github.com/CodesWhat/drydock/compare/v1.3.9...v1.4.0
[1.3.9]: https://github.com/CodesWhat/drydock/compare/v1.3.8...v1.3.9
[1.3.8]: https://github.com/CodesWhat/drydock/compare/v1.3.7...v1.3.8
diff --git a/Dockerfile b/Dockerfile
index de37df74..2f34c401 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,7 +12,7 @@ ENV WORKDIR=/home/node/app
ENV DD_LOG_FORMAT=text
ENV DD_VERSION=$DD_VERSION
-HEALTHCHECK --interval=30s --timeout=5s CMD ["sh", "-c", "if [ -z \"$DD_SERVER_ENABLED\" ] || [ \"$DD_SERVER_ENABLED\" = 'true' ]; then curl --fail http://localhost:${DD_SERVER_PORT:-3000}/health || exit 1; else exit 0; fi"]
+HEALTHCHECK --interval=30s --timeout=5s CMD ["sh", "-c", "if [ -n \"$DD_SERVER_ENABLED\" ] && [ \"$DD_SERVER_ENABLED\" != 'true' ]; then exit 0; fi; if [ \"$DD_SERVER_TLS_ENABLED\" = 'true' ]; then curl --fail --insecure https://localhost:${DD_SERVER_PORT:-3000}/health || exit 1; else curl --fail http://localhost:${DD_SERVER_PORT:-3000}/health || exit 1; fi"]
# Install system packages, trivy, and cosign
# hadolint ignore=DL3018,DL3028,DL4006
diff --git a/README.md b/README.md
index be47edfa..ebd4545d 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
@@ -82,7 +83,7 @@ docker run -d \
> node -e 'const c=require("node:crypto");const s=c.randomBytes(32);const h=c.argon2Sync("argon2id",{message:process.argv[1],nonce:s,memory:65536,passes:3,parallelism:4,tagLength:64});console.log("argon2id$65536$3$4$"+s.toString("base64")+"$"+h.toString("base64"));' "yourpassword"
> ```
>
-> Legacy `{SHA}` hashes are accepted but deprecated (removed in v1.6.0). MD5/crypt/plain htpasswd hashes are not supported.
+> Legacy v1.3.9 Basic auth hashes (`{SHA}`, `$apr1$`/`$1$`, `crypt`, and plain) are accepted for upgrade compatibility but deprecated (removed in v1.6.0). Argon2id is recommended for all new configurations.
> Authentication is **required by default**. See the [auth docs](https://drydock.codeswhat.com/docs/configuration/authentications) for OIDC, anonymous access, and other options.
> To explicitly allow anonymous access on fresh installs, set `DD_ANONYMOUS_AUTH_CONFIRM=true`.
@@ -92,10 +93,8 @@ See the [Quick Start guide](https://drydock.codeswhat.com/docs/quickstart) for D
-
📸 Screenshots
+
📸 Screenshots & Live Demo
-
-Dashboard
Light
@@ -106,81 +105,16 @@ See the [Quick Start guide](https://drydock.codeswhat.com/docs/quickstart) for D
-
-
-Containers
-
-
-
Light
-
Dark
-
-
-
-
-
-
-
+
-
-Container Detail
-
-
-
Light
-
Dark
-
-
-
-
-
-
-
+**Why look at screenshots when you can experience it yourself?**
-
-Security
-
-
-
Light
-
Dark
-
-
-
-
-
-
-
+
-
-Login
-
-
-
Light
-
Dark
-
-
-
-
-
-
-
+Fully interactive — real UI, mock data, no install required. Runs entirely in-browser.
-
-Mobile Responsive
-