---
title: Changelog
description: Release history
canonical: https://cetacean.mazetti.me/changelog
---

# Changelog

All notable changes to Cetacean are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.17.0] - 2026-10-20

### Added
- The History page loads older events past the latest 200, and its name filter searches the whole log rather than the loaded page
- `GET /history` takes `search` and a `before` cursor, and a full page links the next one with `rel="next"`
- `server.version_check` turns off the call to `api.github.com` for the latest Docker Engine release
- The nav bar links the plugin list, and `g` `p` opens it
- The consent page shows the host an MCP client's `https://` client ID lives on, beside the name the client gives itself

### Changed
- **Breaking:** `Allow` lists a resource's own methods, not its sub-resources'. A detail response links each sub-resource you may write with `rel="edit"`, and `Accept-Patch` moves to the sub-resources that take a `PATCH`. API clients reading writes off a parent's `Allow` must read the links
- **Breaking:** `?filter=` refuses a field the resource doesn't have, or a comparison of mismatched types, with `400` (`FLT002`). Task `exit_code` is a number, `0` until the task ends: write `exit_code != 0`, not `"0"`
- **Breaking:** a failed `POST /-/resync` answers with problem details: `504` (`ENG005`) when Docker didn't answer in time, otherwise `502` (`ENG006`) naming what each list answered. Clients must read `detail`, not `error`
- A Client ID Metadata Document is cached as its response's `Cache-Control` or `Expires` allows, for at most a day, instead of always an hour

### Fixed
- The Swarm page no longer offers the CA and encryption editors at operations level 2, which the server refused
- A plugin whose name has a `/`, like `vieux/sshfs`, resolves behind `server.base_path` and with a `.json` suffix
- The demo's topology view shows each service's running replicas instead of reporting every service down
- The resync button reports a failed re-sync, and waits long enough for a slow Docker Engine to answer instead of giving up after 30 seconds
- A method a path doesn't serve answers `405` with `Allow`, and an unknown or disabled `/-/` endpoint `404`, instead of serving the dashboard
- Remove Node in the command palette tries a normal removal first, and offers to force it, behind a second confirmation, only if Docker refuses
- Stopping Cetacean ends open metrics streams, log follows and MCP listen sessions, so a connected client no longer holds shutdown for the whole grace period
- A failed config or secret create is reported once, in the dialog, instead of also as a toast
- MCP `resources/read` of an unknown ID answers `-32602`, or `-32002` on protocol 2025-11-25, instead of an internal error, and names the URI it couldn't find
- An MCP client can no longer cancel another caller's in-flight request by sending `notifications/cancelled` with the same request ID

## [0.16.0] - 2026-10-17

### Added
- `DELETE /tokens/{subject}` revokes every client a user authorized through the authorization server, for when they leave a group or lose access upstream

### Changed
- **Breaking:** `POST /plugins` and `POST /plugins/{name}/upgrade` need the `privileges` the check returned, and answer `409` if the plugin now asks for others. API clients must send the reviewed list
- **Breaking:** writes with no `GET` at their own path, such as `PUT /services/{id}/scale` and the `/swarm` updates, refuse any `If-Match` with `412` instead of ignoring it. Clients must stop sending the header there
- **Breaking:** below operations level 3, a service can mount a named volume only if the manager holds it as a plain `local` volume, so a bind-backed volume can't reach the host. Mounting a volume that lives only on a worker now needs level 3

### Fixed
- Event streams, log tails and MCP listens opened with a token the authorization server issued close when the token expires, instead of running on after it
- A level-2 change to a service's mounts, secrets, configs or networks is refused if the service changed since Cetacean last read it, so it can't restore a bind mount or attachment that was just removed
- Tokens the authorization server issues carry the grants `acl.tailscale_capability` or `acl.headers_acl` gave you at consent, so API and MCP clients get the access your browser session has
- `GET /auth/whoami` accepts a bearer token the authorization server issued, instead of refusing it as invalid
- Behind `tailscale serve`, a name or capability grant with non-ASCII characters is read correctly instead of showing encoded or granting nothing
- Fixed the container health check failing in `cert` mode when Cetacean terminates TLS. A client without a certificate reaches only the unauthenticated endpoints
- Fixed an MCP client on protocol 2025-11-25 being able to list other callers' tasks by padding a `tasks/list` request
- MCP write tools refuse an identifier that isn't a known ID or name, so a truncated ID can't reach a resource outside the caller's grants, and a denial no longer names a resource the caller can't read
- The `Allow` header on a task and on a node with no hostname offers exactly the writes their grants permit
- `GET /api/asyncapi`, `/api/context.jsonld`, `/metrics/status` and `/auth/whoami` answer 406 to a request that accepts none of their types, instead of sending JSON anyway
- Fixed a list request whose `Range: items` header ends at the largest integer answering 500 instead of its first page
- A `304 Not Modified` on a list no longer names a compressed variant the request didn't accept, which could let a shared cache serve it to a client that can't decode it
- Fixed a service's or task's history, and its Atom feed, repeating the newest entry, which could send a feed reader paging in a loop
- Fixed `POST /-/resync` sometimes restoring a resource deleted while it ran, and a hung Docker Engine stalling every later sync; a full sync now gives up after two minutes
- Fixed the dashboard missing a cluster resync that happened just after its event stream reconnected
- One caller can no longer take every log read or log stream slot: each gets a quarter of the cap, as with event streams
- Stack removal and swarm updates no longer repeat the Docker Engine's error text unless it is a validation or availability error you can act on
- The error Cetacean reports when no swarm list answers mentions a swarm that has lost its leader, not only a `docker.host` naming a worker
- The API reference describes a service's resources, placement, update policy and rollback policy as they are sent, with Docker's field names
- `PATCH /services/{id}/resources` answers an unset requirement with `{}`, as the `GET` does, instead of `null`
- `PATCH /services/{id}/mounts` answers a body that isn't JSON with `API008`, like the other merge-patch endpoints
- Fixed an open editor on a detail page carrying its draft over when you navigate straight to another resource of the same kind

## [0.15.0] - 2026-10-16

### Added
- `acl.headers_capability` reads header-mode grants from a JSON object keyed by capability, so grants can come from the tailnet policy through `tailscale serve`'s `Tailscale-App-Capabilities` header
- A History page at `/history` lists the cluster's recent changes, filterable by type and name, with a CSV download of the full log
- `docker.tls.ca`, `docker.tls.cert` and `docker.tls.key` connect to a `tcp://` Docker Engine over TLS, with a client certificate for one started with `--tlsverify`
- A stack or a service can be exported as a Compose file, from its detail page or by adding `.yaml` to its URL. Secrets and configs are referenced, never exported
- `GET /api/asyncapi` describes every SSE stream as an AsyncAPI 3.0 document: its channels, messages, and cursor format
- Both API descriptions are served as YAML as well as JSON, at `/api/openapi.yaml` and `/api/asyncapi.yaml` or by negotiating on `Accept`
- Any list can be downloaded as CSV—add `.csv` to the URL or ask for `text/csv`. Search, filters, and sorting apply; a download that asks for no page gets every row
- The dashboard is installable as an app, with icons and a theme color that follows its own background
- The cluster can be searched from the browser's address bar, via the OpenSearch description at `/opensearch.xml`
- `/.well-known/api-catalog` (RFC 9727) lists the APIs this process serves; every response links to it
- `GET /` answers a JSON client with an entry point naming every collection, so a deployment's address is enough to find the rest of the API; browsers still get the dashboard
- Resources can be addressed by name as well as by ID: `GET /services/shop_web` redirects to its ID-addressed URL. The redirect keeps the method and body, so writes work by name with `curl -L`
- Write operations accept an optional `If-Match` header and refuse with 412 if the resource changed since you read it
- `Prefer: wait` holds a service write open until the cluster settles, and `Prefer: respond-async` acknowledges it immediately
- JSON, Atom feeds and topology exports above a kilobyte are served as zstd or gzip when the client accepts one
- Cross-site request forgery protection covers every write. An origin must be in `server.cors.origins`; `server.public_url` is trusted automatically. A wildcard can't grant writes, so a read-everything deployment must now name its writers
- Client certificate authentication works behind a TLS-terminating proxy that forwards the certificate in `Client-Cert` (RFC 9440)
- `server.forwarded_headers` names which forwarding headers a trusted proxy writes—`x-forwarded` by default, or `forwarded` for RFC 7239. The family it doesn't name is discarded, so a client can't name its own address
- The authorization server publishes the public key that verifies its access tokens, as a JWK Set at `/oauth/jwks`. Anything checking a token Cetacean issued no longer needs a key that could issue one
- `GET /-/health` reports whether Cetacean is still tracking the cluster, and `/-/metrics` says the same for alerting. The dashboard marks itself stale instead of showing a frozen cluster as a live one
- `POST /-/resync` and `GET /swarm/plugins` appear in the API specification
- The REST API accepts bearer tokens the authorization server issues, so a script or app can authenticate without a browser session—set `oauth.api_tokens` to offer it
- `oauth.token_operations_level` holds a token-authenticated caller of the web API below the tier the deployment runs at, so a token left on a device can read the cluster without changing it
- The documentation site is navigable by an agent: every page has a Markdown version, `/llms.txt` lists the site, and `/openapi.json` describes what it serves
- A stack's page and a service's Traefik labels are each drawn as a graph
- `GET /nodes/{id}/drain-impact` says which services a drain would move and which it would strand, and why; the dashboard's drain confirmations show it
- Each protected resource names itself in its discovery document, so a client's consent screen can show `Cetacean MCP` instead of a URL
- The Docker Hub image carries the same build provenance as the GHCR one, so `gh attestation verify` works for either

### Changed
- **Breaking:** adding a bind mount, named pipe, driver-configured volume or Linux capability to a service needs operations level 3, through the API, the dashboard and MCP; ones it already has stay editable at level 2
- **Breaking:** the bundled monitoring stack no longer publishes Prometheus on port 9090 of every node; Cetacean reaches it over the overlay, and the monitoring docs show how to open its UI
- **Breaking:** under an ACL policy, `GET /metrics`, its stream, and `GET /metrics/labels` need `read` on the bare resource `*`; anyone else gets `403` and the dashboard leaves out its charts. MCP `get_metrics` is unchanged
- Demoting a manager down to the bare quorum asks for confirmation first
- An open editor asks before the page unloads, including the sign-in redirect after a session expires
- Attaching a secret in place of a removed one targets the removed secret's path by default
- The service Resources panel shows the generic resources a service reserves, such as a graphics card
- **Breaking:** an `ssh://` `docker.host` refuses startup instead of failing on DNS; it never connected. Forward the engine's socket over SSH, or use `tcp://` with `docker.tls`
- **Breaking:** MCP's `remove_task` needs operations level 3, matching `DELETE /tasks/{id}`; it was available from level 1
- **Breaking:** the OAuth authorization server is opt-in—set `oauth.enabled`. Under any auth mode but `none`, MCP needs it or the active mode named in `mcp.auth_bypass`; startup refuses with neither. An mTLS deployment now runs no authorization server
- **Breaking:** the authorization server's settings moved to their own `[oauth]` section and `CETACEAN_OAUTH_*` variables: `issuer`, `signing_key`, the three TTLs, `require_resource_indicator`, the `dcr_*` trio and `cimd_enabled`
- **Breaking:** a setting the schema doesn't know refuses startup and is named, rather than being ignored—a config file still carrying `[mcp.oauth]` won't start
- **Breaking:** an unknown `CETACEAN_*` variable, a boolean other than `true`, `false`, `1` or `0`, or an unknown `logging.level` or `logging.format` refuses startup. Check your environment for typos before upgrading
- **Breaking:** `auth.headers.trusted_proxies` is gone—use `server.trusted_proxies`, which headers mode already required
- **Breaking:** `mcp.oauth.auth_bypass` is now `mcp.auth_bypass`, and accepts only `cert`, `headers`, and `tailscale`—a listed mode authenticates `/mcp` on its own, so `oauth.enabled` can stay off
- **Breaking:** `/mcp`'s protected resource metadata moved to `/.well-known/oauth-protected-resource/mcp`, so one document describes one resource. A client following `resource_metadata` from the 401 is unaffected; one that hardcoded the root path isn't
- **Breaking:** `mcp.operations_level` narrows `server.operations_level` instead of replacing it. Set above the global tier it now grants nothing; set below, it caps as before
- **Breaking:** refresh tokens and approvals now live in `oauth-tokens.json` under `storage.data_dir`. The former `mcp-tokens.json` isn't read—delete it, and every client authorizes once more
- **Breaking:** the `refresh_token` grant at `/oauth/token` requires `client_id`; a request without it is refused with `invalid_request`
- Clients that registered dynamically stay registered across a restart, instead of having to register again
- `/oauth/register` rejects a client name over 256 bytes, more than 10 redirect URIs, or a redirect URI over 2048 bytes
- `/oauth/register` accepts only `token_endpoint_auth_method: none`; any other value is refused rather than stored and treated as public
- **Upgrade note:** `X-Forwarded-Proto` and `X-Forwarded-Host` are honored only from an address in `server.trusted_proxies`. Behind a proxy without it set, absolute URLs now name the internal address—set `server.public_url` or list the proxy
- Search, the resource lists and the topology view are faster on clusters with hundreds of services, and a stack's event stream costs less per connected browser
- The dashboard's first load is about a third of its former size, and hashed assets are cached permanently
- The API reference at `/api` is six months newer, and now follows Scalar releases automatically
- Access tokens follow the RFC 9068 `at+jwt` profile. Clients holding an older token refresh automatically
- The Cetacean API description moved to `/api/openapi.yaml`; `/openapi.json` now describes the documentation site itself
- **Breaking:** `CETACEAN_OAUTH_SIGNING_KEY` is now a root secret both keys derive from, and must be 32 bytes of hex or base64—generate one with `openssl rand -hex 32`. Leaving it unset still generates a key at startup
- Access tokens are signed with ES256 rather than HMAC, and the key is derived differently, so the published `kid` changes. Clients refresh once on upgrade; stop every replica before starting the new version
- An endpoint with only one representation no longer answers 406 to an `Accept` header it doesn't recognize
- **Breaking:** `server.operations_level` defaults to `0`, read-only. A deployment that never set it could perform operational writes, and on `auth.mode=none` could do so unauthenticated—set it to `1` to keep that
- **Breaking:** a refused request answers `403` rather than `401` under `cert`, `tailscale`, and `headers`—no challenge can ask for the credential those modes read
- Signing in from an address ending in `.atom`, `.csv`, or `.json` returns to that resource's page, not the file
- `PATCH /services/{id}/secrets` and `/configs` take the name, and the default mount path, from the secret or config the ID names; `secretName` and `configName` in the body are ignored
- MCP's `cetacean://cluster` and `get_cluster_status` need at least one grant, as `GET /cluster` does
- Over MCP, a resource the caller may not read is reported as not found, as the documentation promised, rather than as denied

### Removed
- `PUT /services/{id}/mode`, and the mode switch in the service view it drove. Swarm refuses every service mode change, so both could only ever fail. `GET /services/{id}/mode` is unaffected

### Fixed
- tsnet mode serves the app over HTTPS with the node's certificate, and `/-/metrics` on `server.listen_addr` for in-swarm scrapers. Before upgrading, unset `tls.cert` and `tls.key`, and set `server.public_url` if `oauth.enabled` is on
- `GET /swarm` and the Swarm page reveal the join tokens only at operations level 3, to callers with write on `swarm:cluster`
- Attaching a secret, config, or network to a service through the API needs read access to it, as it already did through MCP. An ID Cetacean doesn't know answers `400 SVC021`, and a network name several networks share answers `409 API015`
- Creating a config or secret through the API checks the write grant on its name, so a grant like `secret:team-*` can create `team-db`
- A service port with no `TargetPort` is refused with `400 SVC020` instead of being published to nowhere
- `cetacean healthcheck` probes the server its config file, flags and TLS settings describe, including a listen host and tsnet mode with `server.base_path`; flags may precede it, and unknown subcommands are refused
- With `server.cors.origins` set to `*`, `/mcp` refuses a cross-site browser origin, as the docs say, instead of accepting every origin
- `/mcp` refuses a request body over 1 MiB with `413`, and more than 64 open `subscriptions/listen` streams with `429` and `Retry-After`
- Saving the structured Traefik editor no longer deletes labels it doesn't show, such as TCP routers or `tls.options`, and a save without changes writes nothing
- A Traefik `tls.domains[N]` label with a huge index no longer stalls a service's detail page
- Links into the online demo, such as `/demo/nodes`, open the demo when shared or reloaded, its pages update live, and a badge marks the cluster as simulated
- A `?filter=` expression that takes too long, grows a string past 1 MiB, or runs a costly pattern over a list is refused with `400 FLT004`, so one request can no longer hold a CPU core or its memory
- A mount target with a line break can no longer inject services into a Compose export
- A CSV cell that starts like a formula is prefixed with `'`
- Feed entries escape resource names and links
- Grid view, the default on phones, loads further pages as you scroll instead of stopping at the first 50 items
- The cluster overview shows why its data failed to load, with a retry, instead of loading forever
- A task list whose filter hides every task says so and offers to show them all
- `cetacean --help` exits 0 and states the operations level default as 0
- Startup warns when MCP is enabled without TLS outside auth mode `none` and `server.public_url` isn't `https`
- `GET /swarm/unlock-key` is sent with `Cache-Control: no-store` and varies by identity, so no cache keeps the key
- Creating a config or secret answers `201` with a JSON `Content-Type` and is logged as `201`; `/-/ready` sends its `503` as JSON too
- Configs containing accented letters, CJK or emoji display and copy correctly, and configs and secrets containing them can be created; a failed create shows its error in the dialog
- Clearing a CPU or memory limit or reservation in the resources editor removes it from the service
- Force remove asks for confirmation before it runs
- One client disconnecting while the latest Docker version was being fetched no longer fails the check for everyone for five minutes
- `GET /disk-usage` reuses a result for 30 seconds instead of asking the daemon on every request, and a client that disconnects no longer forces the next request to ask again
- Startup refuses `sizing.thresholds` out of order
- Startup warns when `auth.mode` is `none` with writes enabled
- With Tailscale, Cetacean exits if the listener for health checks fails to bind, rather than running without one
- Built with Go 1.27.2 and `golang.org/x/net` 0.60.0, which fix vulnerabilities in the HTTP/2, TLS, and HTTP code the server uses
- The image's license label uses the OCI key `org.opencontainers.image.licenses`
- Concurrent `POST /-/resync` calls share one sync, and full syncs no longer run at once, so an older one can't replace a newer one
- The snapshot is flushed to disk before it replaces the previous one, and isn't written when `storage.data_dir` couldn't be created
- Service pages no longer crash in browsers without `Intl.DurationFormat`
- Icon-only controls (search, segment overflow) have accessible names, the search palette's state dots are announced, the log level filter is labeled, the log search toggles meet the minimum target size, and the mobile menu closes on back navigation
- The skip link on the API explorer page reaches its content
- `GET /events` applies `?types=` and, on reconnect with `Last-Event-ID`, replays missed events instead of always forcing a full sync
- Node, service, and stack streams receive task removals, a stack stream also every removal of `docker stack rm`, and a service- or stack-scoped grant admits them on `GET /events`, its replay, and MCP subscriptions
- Shutdown ends open `/events` and resource event streams first instead of waiting out the grace period and logging an error
- `Accept` media types and parameters, and the `items` range unit, are matched case-insensitively, including when choosing between the OIDC login redirect and a 401
- Under `server.base_path`, a URL with `//` or `.` segments redirects within the deployment instead of out of it
- `/api/errors/{code}` answers an unknown code with a problem document
- `/topology`'s 406 lists every type it serves
- CORS responses vary on `Origin` even when they grant nothing
- In headers mode, a request carrying the subject, name, email, groups, secret or `acl.headers_acl` header twice is refused; a groups header may name at most 256 groups
- Removing an in-use network, volume, config or secret, or a node that is up or a manager, answers `409` with its code; neither it, the MCP tools, nor a stack removal's errors repeat the daemon's text naming what is in the way
- The volume `force` option is described as what it is: an override for driver errors on a local volume, and for use on a cluster volume; the dashboard no longer offers force removal for a volume or a manager node
- Editing the plugin reference after checking its privileges asks for a new check before it can be installed
- Recommendation fix buttons and Force Rotate on the swarm's CA panel no longer appear for callers who can't use them
- Logs of a service with a TTY are read instead of failing with `LOG007` or an empty live tail, and a service's own `--log-opt` labels no longer leave the raw label prefix in every message
- The activity feed, `GET /history` and the history feeds name tasks as `service.slot`, as MCP does, instead of by their IDs
- The consent page adds `frame-ancestors 'none'` to the global Content Security Policy instead of replacing it, and its nonce cookie is scoped to the OAuth paths under `server.base_path`
- The API explorer's policy no longer allows inline scripts, and a caller's `traceparent` can no longer switch off tracing of its own requests
- An OAuth client evicted at `oauth.dcr_max_clients` is logged; the docs describe the eviction as oldest-first, which it is
- Metrics errors no longer name Prometheus's address, and relay its message only for a rejected query. When it's unreachable, fails, or times out, the banner, `GET /metrics/status`, the metrics stream, and MCP `get_metrics` say only that; the cause is logged
- `GET /metrics` and `GET /metrics/labels` answer while Docker is unreachable
- `manager-has-workloads` is raised only when a ready, active worker could take the manager's tasks, and `get_recommendations` no longer offers `update_node` as its fix
- JSON log reads are capped like log streams (`429 LOG009`), and a `Prefer: wait` past its cap is answered as `respond-async` with a `Retry-After`
- Pointed at a swarm worker, or at a manager before its swarm has a leader, Cetacean reports itself not ready until the swarm answers, instead of serving an empty cluster as healthy
- The monitoring banner fits a phone screen, and the bundled monitoring stack pins cAdvisor to a release and digest instead of `latest`
- Without `server.public_url`, the AsyncAPI document is sent with `Cache-Control: private`, since it names the host the request carried
- A conditional request for `/services` or `/tasks` is answered with `304` before the list is built, as the other lists already were
- Grants under `acl.tailscale_capability` now apply
- The OAuth consent page lists every redirect URI the client registers
- The API explorer at `/api` no longer tries to load hosted fonts or send telemetry
- Setting `com.docker.stack.namespace` on a service, config or secret needs write on the stack it names, so a stack grant can no longer move a resource into another team's stack
- Only the identity that started an MCP task can read or cancel it; anyone else is told it doesn't exist
- Recommendation count badges, the commit hash in the footer, gauge subtitles, log task IDs and muted captions are readable at WCAG AA contrast in both themes
- Detail page sections are announced as headings in order, and the Metrics Console query box exposes a valid role to assistive technology
- Fixed task rows briefly showing raw service and node IDs instead of names while tasks update
- The API explorer at `/api` loads under `server.base_path`, and the OpenAPI document's `servers` names the base path
- The OpenAPI document describes `Range` pagination with its 206 and 416 answers, JSON Feed on every feed endpoint, and the versioned media type
- The OpenAPI schema for `DELETE /stacks/{name}` matches its response: removed counts per type and an `errors` list
- `Allow` no longer offers `POST` on a single config or secret, `PUT` on a service's mode, or plugin install below operations level 3; the Plugins page hides Install accordingly
- Applying a drain from the Recommendations page asks for confirmation first, and the demo no longer offers writes a resource doesn't support
- A service restarting in a loop reads `flapping` and counts as unhealthy in MCP's `get_cluster_status`, the overview page and search, instead of `running` whenever a replica is briefly up
- A service's activity, `GET /history` and MCP's `get_events` include its tasks' changes, so a crash-looping service no longer shows an empty timeline
- Job-mode services read `completed` once their run finishes instead of `running` forever, `Prefer: wait` and MCP's `watch` wait for the run, and the dashboard names their mode
- A stack's page lists the configs, secrets, networks and volumes deployed with it straight away, instead of after the next full sync
- Until the first sync, a restart serves the loaded snapshot to JSON, CSV and MCP clients as well as the dashboard, instead of answering `503`
- Memory no longer grows with every task ID the event history has ever seen
- Dashboard pages without charts or graphs no longer download the chart and graph code on first load
- A finding picked in the recommendations widget reaches the model with its resource names and text quoted as a JSON value marked as cluster data
- Saving an editor on a service, node, config or secret no longer overwrites a change someone made after you opened it; you are asked to reload instead
- `GET /search` and MCP's `find` count only matches the caller may read, so a count no longer reveals resources the policy hides
- History feeds, `GET /history`, and `cetacean://history` fill a page with entries the caller may read, so a restricted caller no longer gets an empty one
- `GET /topology`, in every format, no longer names networks the caller may not read
- A config, secret, network or volume page under a scoped grant updates live when a service starts or stops using it
- Creating a config or secret answers with only the services the caller may read
- An `If-Match` update to a service, node, config or secret is refused with `412` if the resource changes while the write is in flight, instead of overwriting the change
- A Client ID Metadata Document is refused when its URL has `.` or `..` path segments, it declares `client_secret_jwt`, or its host resolves to a special-use address
- **Breaking:** fetching a Client ID Metadata Document no longer follows redirects, and a document declaring any `token_endpoint_auth_method` other than `none` is refused instead of treated as a public client, as the draft requires
- A stream refused at its connection cap asks for a different `Retry-After` each time, so clients turned away together no longer return together
- One user, or one address under `auth.mode = "none"`, can hold at most a quarter of the event-stream connections, so a single client can no longer lock everyone else out
- An event stream reconnecting after Cetacean restarts is told to refetch, instead of being replayed unrelated events. Event `id:` values are now opaque strings
- `/mcp` accepts clients on MCP revision `2025-11-25`, which most client libraries speak, without sessions or server-sent notifications. Only older revisions are refused
- An MCP client on a revision newer than `2026-07-28` is told which revisions are supported, without the ones it would be refused on
- Addressing a task as `web.1` reaches the replica running in that slot, not a replaced one Swarm still keeps a record of
- A detail page reached by name, like `/services/shop_web`, shows its activity and updates live instead of sitting empty
- `GET /history` and the MCP `get_events` tool narrow by a resource name as well as an ID, instead of answering an empty timeline
- Both discovery documents are served at the address their RFC has a client derive, as well as under `server.base_path`. Behind a proxy, forward `/.well-known/*` from the host root too
- An access token whose `aud` is an array, or whose `typ` differs only in letter case, is accepted—both are conformant shapes that were refused
- The authorization server advertises that it sends `iss` on authorization responses, so a client actually enforces the mix-up check the responses already carry
- A token request missing `grant_type`, `code` or `refresh_token` is refused with `invalid_request` rather than `invalid_grant`, which told clients to discard a working grant
- A repeated RFC 8707 `resource` parameter is refused with `invalid_target` instead of binding the token to whichever came first
- A 401 names where a token comes from even when the request carried no credential, and no longer calls that `invalid_token`—RFC 6750 reserves that for a token actually sent, so cold discovery works off the challenge
- An ACL grant written against an email address matches a token as well as a browser session. MCP clients were silently denied everything such a grant allowed
- Refresh-token theft detection now runs: a replayed token revokes its grant family and the remembered approval
- Everything that doesn't describe the cluster keeps working while the Docker daemon is unreachable—the dashboard's own icons and manifest, the API catalog, the OpenSearch description, `/profile`, and the OAuth endpoints that issue a token
- A Docker Engine too old for Cetacean says so at startup instead of coming up and serving empty pages. Cetacean speaks Docker API 1.46, which means Engine 26.1 or newer
- A recommendation that measured zero no longer reads as one that measured nothing—a service using essentially no CPU reported an empty `current`
- Header-based authentication works behind a reverse proxy again; it was answering 401 to every request
- Asking an endpoint for a format it doesn't serve now says so, instead of answering with JSON
- A malformed `filter` is reported as an error again on a request carrying `If-None-Match: *`
- The authorization response redirects with `303 See Other` rather than `302 Found`
- A redirect URI carrying a fragment is refused at registration
- A write carrying `If-Match` against a resource that no longer exists answers `404`, not `412`—the validator wasn't what was wrong with it
- Atom feed identifiers lowercase the host and move the port out of it, so one feed reached two ways is one feed, and two deployments on one host stay two
- An address matching no route answers `404` with a problem document rather than `200` and the dashboard—on a write, and on a read from a client that said it can't use a web page
- A format an `Accept` header rules out with `;q=0` is refused with `406` instead of served anyway
- `/favicon.ico` and the dashboard's other static files are no longer refused with `406` when a client asks for them as an image
- Deep links into a service whose name contains a dot, such as `/services/web.api/logs`, open instead of answering 404
- `/index.html` opens behind a hardened reverse proxy, instead of redirecting to a path nginx and Apache refuse
- A transient hiccup in the five-minutely re-sync no longer reports the cluster as unreachable for the next five minutes. The stream ending is what marks a disconnection; a stalled cache still shows as stale
- A cluster operation run as an MCP task can no longer outlive the process. Detaching it from the request dropped its deadline too, so a wedged Docker call held its goroutine and its connection open forever
- A write that loses a race answers `409` naming the conflict, instead of a bare `500 Docker Engine Error`
- A `PATCH` to a service's resources, health check, update policy, rollback policy, log driver, or container config no longer discards an edit made just before it
- Every identifier a response hands out works under `server.base_path`. Listings and a task's links to its service and node no longer lead outside the deployment to a 404
- Relabeling a node needs operations level 2 over the API, matching MCP. It was gated with draining and demoting
- `POST /-/resync` requires authentication but not an operations level, so a read-only deployment keeps its refresh button
- A task no longer sits at `starting` for minutes after it's running, which also left the running count short and anything waiting for a service to settle waiting
- Recommendations are complete right after a restart, instead of missing every sizing finding for the first five minutes
- A rolling update no longer truncates whatever was in flight: shutdown waits up to five seconds for in-flight requests
- Distributed tracing exports spans. `tracing.endpoint` takes the collector's base URL, and every export went to `/` rather than `/v1/traces`, where a collector discards it
- The two log reads carry an `ETag` and `Cache-Control`, so a client polling their cursors can revalidate
- JSON Feed responses are labeled `application/feed+json` rather than plain JSON, so a reader can tell they're subscribable
- An Atom feed identifies itself by the same host its links use. Set `server.public_url` to keep that identity stable across proxies, since nothing derived from a request can be
- Feed alternate links carry only the parameters the feed they point at reads
- A client that subscribes over MCP only to "the list changed" is notified again; with an authorization policy configured it was silently never told anything
- A service change asked for through the MCP tasks extension reaches the engine. It never did, and reported back as `cancelled`
- Authorization policy changes are picked up when the file is replaced by a rename, as a deployment, several editors, and a ConfigMap update all do
- The error reference page at `/api/errors` renders instead of showing "Something went wrong"
- The API documentation, the playground script, the JSON-LD context and the attribution documents are cacheable
- `/oauth/authorize` no longer answers cross-origin requests, whatever `server.cors.origins` allows, so a page on an allowed origin can't read the consent form

## [0.14.0] - 2026-09-10

### Added
- `server.public_url` sets the canonical external URL once, instead of configuring the MCP OAuth issuer and the OIDC redirect separately
- The documentation site carries an error reference: every code the API can return, its status, meaning, and resolution

### Changed
- Deploying as a stack no longer needs an overlay network created first—`docker stack deploy -c compose.yaml cetacean` stands alone. Add `-c compose.prometheus.yaml` to connect the bundled Prometheus
- Status colors are consistent across the dashboard and adapt to dark mode, drawing from one palette of five meanings
- The service and stack lists head their rollout column **Rollout** rather than **Status**, and no longer color a settled rollout green—it reports Docker's update state, not whether the service is running
- MCP tools advertise the values each argument accepts, so a client can offer them. `get_logs` gained the `fatal` level
- The MCP tool reference gives every tool its own linkable entry rather than a table per operations level
- The topology view draws its graph immediately instead of waiting for the layout engine to download
- A dropped connection or server restart no longer leaves a page stuck on an error; transport failures are retried briefly

### Fixed
- Metrics charts are live again—none of them ever opened the streaming connection, so they only moved on the refresh timer
- Keyboard navigation in resource tables no longer loses its place when an update arrives over the live connection
- Searching a live log keeps your place; stepping through matches was pulled back to the first hit on every new line
- Service cards in the topology view show how many replicas are actually running, and global services report tasks up rather than 0/0
- The topology view no longer labels every service "Updating…"—Docker leaves a finished rollout on a service indefinitely
- Stacks in the topology view can be told apart by color again
- The dashboard has a favicon, and dark mode no longer flashes white before the page appears
- The dashboard loads in a browser configured to block site data, falling back to defaults instead of a blank page
- Resource tables can be sorted from the keyboard, and screen readers are told which column is sorted and which way
- Form labels across the integration panels, mount editor and swarm settings are tied to their fields, so a screen reader announces both label and hint
- The search palette and shortcuts overlay hold keyboard focus while open and return it on close
- Service cards and edge-tooltip network links in the topology view can be reached and opened with the keyboard
- Pressing Enter submits the create-config and create-secret dialogs
- The keyboard shortcuts overlay shows that its list continues below
- Alternating row shading no longer inverts while scrolling a long table or log
- The confirmation button in the command palette is legible again
- The documentation site's "Edit this page on GitHub" link points at the file a page was written from, and diagram pan and zoom controls are announced to screen readers
- Startup no longer stops for every authenticated deployment over an unreachable MCP OAuth issuer—only when MCP sign-in is actually in use
- Atom and JSON Feed links are built from `server.public_url` when it's set, rather than from request headers
- `make build` works from a fresh clone; it previously failed because the binary embeds a frontend that had never been built
- The example config offered `acl_claim`, `acl_capability`, and `acl` keys that don't exist, so anyone uncommenting one got no grants and no warning. The working keys are `oidc_claim`, `tailscale_capability`, and `headers_acl` under `[acl]`

## [0.13.0] - 2026-09-07

### Added
- MCP clients that support interactive apps render results as widgets: a sortable table, a topology graph, a live log tail, a metrics chart, and recommendations grouped by severity
- AI agents can ask what's using the most CPU, memory, or network, cluster-wide or on one node
- AI agents can ask what draining a node would move, where it would go, and which services have nowhere to go
- AI agents can ask whether the cluster is healthy and be told what's wrong—degraded services and nodes, rollouts in flight, capacity reserved
- AI agents can ask what changed and when, narrowed to a time range, a resource type, or one resource
- AI agents can read and search logs across a stack or the whole cluster in one call, each line naming its service
- AI agents can wait for a service to finish deploying, and are told how far the rollout got if it never settles
- AI agents can change a service's mounts, secrets, configs, health check, and command, and create secrets and configs—completing the rotation sequence. A secret's value is never returned by any tool
- AI agents can chart CPU, memory, or network for a service or node over the last hour, six hours, day, or week. Requires Prometheus
- AI agents can ask for Cetacean's recommendations directly, optionally only the critical ones
- AI agents can list a whole resource type, paged, rather than searching one name at a time
- MCP clients are offered named investigations—diagnose an unhealthy service, explain why one won't schedule, review capacity, roll back, right-size, drain a node—filtered to what the caller could carry out end to end
- MCP clients can complete a resource or prompt argument, offering names rather than IDs and only resources the caller may see
- Resources can be addressed by name as well as by ID—`cetacean://services/web`. An ambiguous name is refused, naming the candidates
- `find` and `describe` return links to the resources they're about
- Approving an MCP client is remembered for 90 days instead of being asked on every expiry. `CETACEAN_MCP_CONSENT_TTL=0` always asks
- Cetacean exports traces to an OpenTelemetry collector via `CETACEAN_OTEL_ENDPOINT`, joining a calling agent's trace where there's one
- Cetacean identifies itself to MCP clients with a display name, website, and icon
- `CETACEAN_MCP_SIGNING_KEY_FILE` reads the signing key from a file, so it can be supplied as a Docker secret
- MCP list and read responses carry cache freshness hints, so agents poll less
- The licenses page shows the full text and NOTICE of every bundled dependency, filterable by license and ecosystem, with the whole attribution document as a download

### Changed
- **Breaking:** the MCP server speaks protocol revision 2026-07-28 only. Older revisions are refused with an error naming the version to use—update your client if it can't negotiate it
- **Breaking:** an AI agent receives a compact description of a resource rather than raw Docker output. Listing and searching are now one `find` tool, with a new `describe` for detail; raw records are still available on request
- **Breaking:** the eleven MCP tools that edited a service or node specification are now `update_service` and `update_node`, naming the part they change in a `section` argument. `update_node_labels` stays separate because it needs a lower operations level
- **Breaking:** `update_service` no longer accepts `secrets`, `configs` or `mounts` as a `section`. These reported success and changed nothing; each now has its own tool
- **Breaking:** the MCP tools that edit a specification return a compact summary of the section they changed rather than the whole Docker object, and every tool now publishes an output schema
- **Breaking:** the MCP tools that scale, update the image of, roll back or restart a service return where the service ended up, not its full specification
- **Breaking:** the MCP `search` tool returns `results`/`counts`/`total`, matching the HTTP search response
- **Breaking:** `/topology/networks` and `/topology/placement` are removed. `/topology` has replaced them since 0.12.0 and both paths answer `410 Gone` naming it
- **Breaking:** `CETACEAN_MCP_SESSION_IDLE_TTL` and `CETACEAN_MCP_MAX_SESSIONS` are removed—the protocol has no sessions. Both are ignored if still set
- **Breaking:** an MCP signing key shorter than 32 bytes is refused at startup rather than used. Leaving it unset still generates a random one
- MCP authorization responses identify the issuer (RFC 9207), so a client can't be tricked into redeeming a code at the wrong one
- Client ID Metadata Documents are now the recommended way for MCP clients to identify themselves, and are advertised as such. Dynamic Client Registration still works
- MCP clients registering dynamically declare whether they're a native or web application, and are held to the redirect URIs that implies
- A cross-type MCP search reports how many matches each resource type holds alongside the total
- The documentation has been rewritten end to end, and the MCP page split into a setup guide and a tool reference

### Fixed
- **Security:** the MCP tools that edit a service's specification no longer return its environment variable values or log-driver options—an agent raising a CPU limit was handed the service's credentials
- An MCP client whose permissions match no grant no longer receives change notifications, which leaked the timing of changes it can't see
- Listing tasks over MCP no longer names a service or node the caller has no permission to read
- Per-service charts and every sizing recommendation were silently empty on the bundled monitoring stack. cAdvisor reads container metadata through containerd, whose socket is now mounted
- The bundled monitoring stack applies its own Prometheus configuration; it was mounted where Prometheus doesn't read it, so no exporters were ever discovered
- Node charts show data when Prometheus scrapes the exporters over an overlay network
- The monitoring stack and Cetacean's stack share the overlay network they're documented to share, and `docker stack deploy -c compose.yaml cetacean` works on a fresh swarm
- Cetacean no longer recommends shrinking services it has no measurements for; missing figures were read as a measured zero
- Right-sizing recommendations state current, configured, and suggested CPU in the same unit—a suggestion could look like a thousandfold change
- Replica counts are correct for services that restart frequently. Tasks Swarm had replaced were counted as running until the next re-sync, so a crash-looping service could report thirteen replicas against a desired one
- A service restarting in a loop is reported as failing rather than healthy, and appears in the cluster's list of what's wrong
- Waiting for a deploy no longer reports failure on a rollout that succeeded, nor success the instant it starts
- The topology view includes services published with DNS round-robin, which have no virtual IP and so appeared connected to nothing
- The physical topology view no longer counts a replaced replica twice
- `/topology` returns a stable ETag, so a conditional request is answered "not modified" instead of re-sending the graph
- Live pages, charts and the connection indicator recover when the server is momentarily at its connection limit, instead of staying silent until reload
- Live log tail reconnects on its own, resumes where it left off, and shows what it's doing; at the server's connection limit it counts down rather than showing a cryptic error
- Live log tail no longer sits empty behind a "Live" badge when the time range is a duration, at whole-second precision, in a non-UTC zone, or the local clock runs ahead
- Multi-line log output is no longer repeated in full on every reconnect, and is attributed to the task that produced it
- Log lines from services with several replicas are no longer dropped or shown twice around a reconnect
- Live log tail sends you to the login page when your session expires
- Log reads say when they couldn't reach the start of the requested window, or when a search ran out of budget before the older lines
- Searching within a live log, and the change timeline, now report how far back they can answer for
- Browsers no longer reconnect in lockstep after a restart, which made them collide and synchronize harder each round
- Charts plotting a single value no longer drop to zero on every live update, and keep streaming after a tab switch
- Resources created while a list page is open appear in the list, instead of the page growing by a repeated row
- Lists hold a consistent order, so returning to one after visiting a resource no longer means hunting for the row you clicked
- The Tasks page no longer flips between the list and a "range start is beyond the total number of items" error
- Breadcrumbs on a resource in a stack lead through the stack, and a resource that belongs to none no longer shows a stack prefix that isn't there
- Plugins on the Swarm page show their type instead of `undefined.undefined/undefined`, and scoped npm packages on the licenses page show their full name
- The keyboard shortcuts overlay lists every shortcut, and describes `/` as opening the command palette
- Editing a service's ports by reading them and writing them back no longer destroys the published port. Any section given a field it doesn't have is now refused, naming it
- The change history for a single resource no longer stops after 64 entries while reporting itself complete
- Label keys containing a hyphen are accepted when editing a node's labels, as Docker allows
- A service running an image from a private registry on a port no longer links to a Docker Hub page that doesn't exist
- AI agents are told the truth about a change they just made. Scaling reported the service as it was *before* the call, and returned a version the next change would have rejected
- Asking an agent for metrics now says when nothing is collecting them, rather than returning an empty chart indistinguishable from an idle service
- An agent can tell that a service is stuck in a restart loop; a description now carries how often its replicas have died in the last hour and week
- Recent change history over MCP identifies a task by its service and slot instead of repeating the task's own ID
- Asking for a dead replica's logs over MCP explains that the output is gone for good and names the service to read instead
- **Breaking (MCP):** a service description reports its entrypoint as `command` and its arguments as `args`, the split Docker itself makes. The entrypoint was previously hidden entirely
- **Breaking (MCP):** the cluster overview states every figure in a named unit and gives both CPU numbers in cores. Reserved CPU was in NanoCPUs, so an agent comparing the two was wrong by a factor of a billion
- **Breaking (MCP):** a recommendation that can be acted on names the tool that does it, rather than an HTTP route an agent can't call
- A service description reports a health check's probe command, timeout, start period and retry count, not just its interval
- Asking an MCP tool for a raw Docker record no longer breaks the shape it promised; raw records come back alongside the compact answer rather than in place of it
- Addressing a task by name over MCP works for services whose name contains a dot
- Reading a service's logs over MCP by name works on deployments that use an authorization policy
- AI agents reading service logs over MCP advance through history correctly, and receive at most the number of lines they asked for
- Stack-scoped permissions reveal the MCP tools and investigations they cover, and the change notifications for services inside the stack
- An MCP client whose permissions match no grant no longer sees tools it can't use
- AI agents on the new protocol revision receive live cluster updates; subscribing succeeded and then delivered nothing
- MCP clients running in a browser can reach the server—cross-origin preflight rejected every header the transport requires
- MCP clients no longer re-authorize on every restart; refresh tokens are kept in the data directory. Set `CETACEAN_MCP_SIGNING_KEY` to keep access tokens valid too
- An agent asking for a service change as a task no longer grows Cetacean's memory indefinitely. A default expiry applies when none is asked for (`CETACEAN_MCP_TASK_TTL`, 15 minutes), capped by `CETACEAN_MCP_MAX_TASK_TTL`
- `CETACEAN_MCP_CONSENT_TTL=0` works as documented; setting it stopped Cetacean from starting
- Turning off `CETACEAN_MCP_CIMD_ENABLED` takes effect; the setting was ignored
- The example Compose file gives Cetacean a volume for its state, and names the published image rather than a local tag
- The Tailscale tsnet example sets the state directory it mounts a volume for, so the node no longer re-authenticates on every restart
- The API documentation no longer describes MCP tools that don't exist, and documented task states, media types, and feed formats match what the server accepts
- The configuration reference documents every setting; the MCP, authorization and sizing settings were absent
- The documentation site publishes again—one page without the required metadata had been failing every build
- Corrected several documentation claims: the log viewer doesn't tail until turned on, the logical topology draws one edge per pair of services, Swarm ignores Compose's `depends_on`, and the default operations level permits operational writes rather than everything
- Release binaries carry build provenance, as the container image already did
- The SBOM no longer ships without the integrity hash of every npm dependency, which the frontend build strips from the installed tree

## [0.12.0] - 2026-08-28

### Security
- Updated Go, dashboard and website dependencies to clear every outstanding advisory, including a critical one in the OpenAPI parser (GHSA-r277-6w6q-xmqw)
- Rejected OIDC bearer tokens no longer reach the server log
- A `Request-Id` header forwarded by a proxy is only echoed back when it's letters, digits, dashes, underscores, and dots; anything else is replaced

### Added
- Embedded Model Context Protocol server, opt-in via `CETACEAN_MCP=true`, exposing cluster state to AI agents at `/mcp`—twelve resources and twenty-three tools across the read, operational, configuration, and impactful tiers
- OAuth 2.1 authorization server for MCP clients, with Dynamic Client Registration, Client ID Metadata Documents, resource indicators, PKCE-only flows, and refresh token theft detection
- `CETACEAN_MCP_ISSUER` sets the canonical OAuth issuer URL when Cetacean runs behind a reverse proxy
- MCP authorization metadata is also served at the OpenID Connect discovery path, for clients that look there
- The MCP endpoint rejects a request carrying a disallowed `Origin` with 403, a DNS-rebinding defense. The allowlist is `CETACEAN_CORS_ORIGINS`; non-browser clients send no `Origin` and are unaffected
- MCP tools return machine-readable results alongside the text form, so an agent need not scrape JSON out of a blob
- Every MCP tool and resource carries a title, a description of when to use it, behavioral hints and an icon, so clients can render confirmation prompts accurately
- The MCP server describes itself on connect: the read-mostly model, that writes are gated by operations tier and ACL, and to resolve a resource before acting on it
- Open-source licenses page, linked from the footer, listing every bundled Go module and frontend dependency with search and per-ecosystem filtering. The bill of materials is at `/-/sbom.cdx.json`

### Fixed
- Resource pages no longer crash on fields the Docker Engine omits—nodes that haven't reported a hostname, platform, resources, or address; tmpfs mounts; unset ports and gateways; services with no image
- The service detail page no longer crashes on a task update arriving over the live stream, or when a service's task template is absent
- Swarm CA and Raft panels no longer show `undefined` for settings Docker omits at their default
- Plugin type displays as `docker.volumedriver/1.0` rather than `[object Object]`
- node-exporter is detected by its metrics, not a Prometheus job literally named `node-exporter`—any other naming hid the node CPU, memory, and disk columns. An unreachable target reports as undetected rather than rendering empty panels
- Metrics charts and the console no longer break on Prometheus responses carrying no samples, and the console renders scalar and string results such as `time()`
- Unexpected errors show a recovery screen with reload and try-again, with technical detail collapsed
- Confirmation dialogs dismiss themselves once the action is confirmed instead of staying open
- Fixed latent crashes in the disk-usage chart tooltip, the log-driver options editor, and keyboard navigation on empty radio-card groups
- Editing a service's environment variables or labels merges against the current spec instead of replacing the whole map, and concurrent writes to unrelated keys are no longer lost
- MCP OAuth discovery works under a base path; a client deriving the metadata URL from the issuer previously got a 404
- `CETACEAN_MCP_AUTH_BYPASS` takes effect, so MCP clients can reach `/mcp` on upstream identity such as mTLS without a bearer token
- MCP `tools/list` hides write tools the caller has no grants for, and change notifications skip clients that can't read the affected type
- MCP ACL checks for logs, task removal and config, secret and network removal now match the REST policy semantics they diverged from
- Stack and volume detail reads over MCP make "denied" and "not found" indistinguishable from outside
- MCP `search` rejects an empty query instead of returning every cached resource, and no longer advertises a `types` parameter it ignored
- `update_service_image` rejects an empty image string
- Destructive MCP tools—restart, rollback, node availability, and role—advertise it, so clients can gate them behind a prompt
- MCP client metadata documents are validated and fetched over a connection pinned to a checked address, so a malicious host can't inject a `javascript:` redirect or reach an internal one
- MCP PKCE verifiers are compared in constant time and held to the length and alphabet the spec requires
- MCP refresh-token history, abandoned authorization codes and registration rate-limit buckets are bounded, so a long-running server no longer accumulates them
- A refresh-token grant family respects its absolute lifetime, and a resource-indicator typo no longer burns the family
- MCP registration rejects unsupported `grant_types` and `response_types` rather than storing them, and caps request bodies
- The MCP consent page no longer surfaces raw fetcher errors, and isn't cached, so a back-button replay can't surface another user's session
- Reading logs over MCP returns promptly when Docker leaves the stream open
- A service is reported as `updating` while a rollback is in progress, not only a forward update
- Website dependencies can be installed again; an unresolvable Astro peer conflict made every `npm install` fail

## [0.11.2] - 2026-05-20

### Security
- Bumped `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` to v1.43.0 to pick up the fix for unbounded HTTP response bodies (GHSA-w8rr-5gcm-pp58)
- Pinned `fast-uri` ≥ 3.1.2 via npm overrides in the dashboard, clearing the path-traversal and host-confusion advisories (GHSA-q3j6-qgpj-74h6, GHSA-v39h-62p7-jpjc)
- Pinned `hono` ≥ 4.12.18, `ip-address` ≥ 10.1.1, `postcss` ≥ 8.5.10 in the dashboard's transitive (dev-only) dependencies
- Updated the marketing site's dependencies via `npm audit fix`, clearing the critical `protobufjs` arbitrary-code-execution advisory (GHSA-xq3m-2v4x-88gg) and the bundled `dompurify`/`astro` advisories

### Fixed
- Production frontend build no longer pulls test files through `tsc`, so test-only Node imports stop breaking the release pipeline

## [0.11.1] - 2026-05-20

### Added
- Atom feed support on resource endpoints—request via `Accept: application/atom+xml` header or `.atom` URL suffix
- Feed icon button in page headers for pages with Atom feeds
- `Link: rel="alternate"` header on JSON responses advertising the Atom feed URL
- API reference shows operations-level badges on write endpoints, an experimental marker on the recommendations endpoint, and human-readable descriptions for enum values (recommendation categories, node availability/role, service mode, auth providers)
- Manual resync button next to the live-connection indicator and `POST /-/resync` endpoint for forcing a full refetch when the cache appears stale

### Fixed
- Service resource charts now scale reservation/limit threshold lines by replica count so they line up with the service-wide usage curve (a 3-replica service with a 1 GB/task limit no longer appears to exceed its limit at 1.5 GB usage)
- Node and task detail pages no longer crash when Docker briefly returns a payload with null Description/Status/Spec fields (the previous `c.Resources is undefined` error)
- Cache no longer drifts after rapid stack deploys: transient inspect failures retry with backoff instead of being silently dropped
- Task SSE events now also fire when a task's status message changes—previously the stream silently coalesced these
- Log viewer follow-mode keeps up with high-volume streams instead of disabling itself a few lines after Jump to Bottom
- Loading older log pages no longer jumps the viewport to a wrong position
- Live log indices are re-stamped after the in-memory buffer trims so search highlight and pinning stay correct
- Late-arriving log fetches can no longer overwrite newer results during rapid filter/time-range changes
- Container-registry icons are bundled inline instead of loaded from `github.com`, `hub.docker.com`, etc.—the previous URLs were blocked by the dashboard's CSP
- Tightened the `img-src` CSP directive now that no external images are referenced
- Filter expressions like `exit_code != "0"` no longer match running tasks (Docker reports `-1` mid-run)
- Task detail CPU/memory gauges fall back to the host node's capacity when the service has no per-task limit set, instead of rendering empty
- Exit code is no longer shown on running tasks (Docker often reports `-1` while a container is alive)
- Node list no longer crashes when a node briefly arrives without a Description payload
- SSE drops caused by slow clients are now surfaced as the `cetacean_sse_client_events_dropped_total` metric so silent drift can be detected
- Corrected OpenAPI spec examples to match actual API responses
- Documented missing OpenAPI endpoints and parameters: `POST /swarm/unlock`, `GET /topology`, `GET /services/{id}/mode`, `GET /services/{id}/endpoint-mode`, `?force` on node and volume deletion, and `application/merge-patch+json` support on env and label PATCH endpoints
- Removed dead `GET /swarm/plugins` alias route
- Empty cross-reference and collection fields now serialize as `[]` instead of `null` on config/secret/network/volume/stack detail, service sub-resources (configs, secrets, networks, mounts), stack listings, and the recommendations endpoint
- Flaky-service warnings now report actual task failures instead of inflated counts derived from a misused Prometheus metric—restarts are tracked from swarm events and persist across restarts via the snapshot. Flaky-service detection no longer requires Prometheus.

## [0.11.0] - 2026-04-02

### Added
- General trusted proxies setting (`CETACEAN_TRUSTED_PROXIES`) for real client IP resolution behind reverse proxies—replaces the headers-auth-specific setting, which is now deprecated
- Client IP in structured request logs when trusted proxies are configured
- Command-line flags for all settings that were previously env-var-only: `-operations-level`, `-sse-batch-interval`, `-cors-origins`, `-snapshot`, `-data-dir`, `-trusted-proxies`
- Tailscale auth mode comparison table in the authentication docs
- Configurable CORS support for cross-origin API access (`CETACEAN_CORS_ORIGINS`)
- Grant-based RBAC authorization with per-resource access control
- `Allow` response header indicating available methods per resource—the dashboard uses this to show or hide write controls per resource and per user
- `Accept-Patch` response header advertising supported patch formats per resource (RFC 5789)
- `Prefer: return=minimal` support on all write endpoints (RFC 7240)
- `Last-Modified` / `If-Modified-Since` conditional requests on resource detail endpoints
- `Strict-Transport-Security` (HSTS) header when TLS is enabled
- Structured error responses (RFC 9457) for all authentication and OIDC callback errors
- SSE keepalive comments on idle connections to prevent proxy timeouts
- Footer with version info, GitHub, docs, and API links
- Table/grid view toggle on the tasks page with tasks grouped by service in card view
- Arrow-key navigation for radio card groups
- Series labels in the Prometheus query result table
- Per-stack CPU and memory usage charts on the node detail page with drill-down to individual services
- HTTP Range Request pagination on all list API endpoints (`Range: items 0-49` returns `206 Partial Content` with `Content-Range`)
- Infinite scroll on all resource list pages—items load automatically as you scroll down

### Deprecated
- `CETACEAN_AUTH_HEADERS_TRUSTED_PROXIES`—use `CETACEAN_TRUSTED_PROXIES` instead; slated for removal in v1

### Security
- Service tasks, service logs, task logs, and node tasks endpoints now enforce ACL read checks—previously accessible to any authenticated user regardless of grants
- Topology endpoints now filter services and nodes by per-resource ACL grants instead of showing the entire cluster to any authenticated user
- History endpoint now filters events by per-resource ACL read permission instead of showing all resource changes
- SSE event replay on reconnect now applies ACL filtering (previously replayed events bypassed ACL)
- Policy validation rejects malformed glob patterns at load time instead of silently creating dead grants
- Monitoring status, Prometheus label names, and label values endpoints moved from unauthenticated `/-/` prefix to authenticated `/metrics/`—previously exposed cluster node count and Prometheus label data without authentication
- Task-to-service-to-stack ACL inheritance chain now resolves correctly (previously stopped at service level)

### Changed
- `X-Request-ID` header renamed to `Request-Id` per RFC 6648 (deprecation of `X-` prefix)

### Fixed
- Layout shift in the header when the live connection timer changes width
- Pressing Escape in a combobox dropdown closing the parent editor panel
- Node metrics table columns, gauges, and chart tooltips when the Prometheus instance label uses a hostname instead of an IP
- Sizing recommendations comparing aggregate usage across all tasks to per-task limits, producing incorrect percentages and suggestions for multi-replica services

## [0.10.0] - 2026-03-31

### Added
- Self-metrics endpoint (`/-/metrics`) documented in monitoring guide with full metric reference
- `CETACEAN_SELF_METRICS` toggle to disable the self-metrics endpoint (enabled by default)
- `CETACEAN_RECOMMENDATIONS` toggle to disable the recommendation engine (enabled by default)
- Task state filter on the tasks page—filter by running, failed, or any other state via segmented control
- "Failed Tasks" card on the cluster overview now links directly to failed tasks
- Recommendation cards expand to show why each recommendation matters
- Monitoring status banner shows the actual error when Prometheus is unreachable

### Fixed
- Relative timestamps ("5 minutes ago") never updating while the page stays open
- Chart data briefly corrupted when switching time ranges on metrics panels
- Task state not updating in real time when replicas finish starting, requiring a 5-minute wait for the periodic re-sync
- Prometheus proxy returning raw 404/502 responses instead of structured errors when the configured URL is wrong
- Prometheus query client ignoring HTTP error status codes, producing misleading "parse error" messages
- Base path detection using page URL instead of `<base>` tag, causing broken API requests when navigating in the dev server
- API requests hanging indefinitely when Prometheus or Docker is unresponsive (now timeout after 30 seconds)
- Empty state messages not announced by screen readers

## [0.9.1] - 2026-03-30

### Fixed
- Assets not loading when deployed under a base path

## [0.9.0] - 2026-03-30

### Added
- Sub-path deployment: serve Cetacean under a configurable URL prefix (for example `/cetacean/`) via `CETACEAN_BASE_PATH` environment variable, `--base-path` flag, or `base_path` TOML config
- Recommendation engine: `/recommendations` page with cluster health checks across resource sizing, config hygiene, operational health, and cluster topology—with one-click fixes for auto-fixable items
- Integration detection: Traefik, Shepherd, swarm-cronjob, and Diun labels shown as structured panels on service detail pages, with inline editing

## [0.8.2] - 2026-03-27

### Fixed
- Release Docker image running as unprivileged user, preventing Docker socket access
- Release Docker image health check not detecting Docker connectivity issues
- Minimal scratch base image for release builds (was alpine)

## [0.8.1] - 2026-03-27

### Fixed
- Resource pages showing empty states instead of errors when Docker socket is unreachable
- Container running as unprivileged user unable to access Docker socket
- Watcher reconnect loop spamming API with sync events on persistent Docker failures

## [0.8.0] - 2026-03-26

### Added
- Label editing for configs and secrets
- Config and secret creation from the dashboard
- Plugin management: browsable list and detail pages, install/upgrade with privilege review, enable/disable/remove/configure
- Swarm configuration editors: per-section editing for raft, CA, orchestration, and task defaults, with token rotation and unlock key retrieval
- Structured error codes across all API error responses with browsable error reference at `/api/errors`
- Toast notifications for mutation errors with contextual suggestions from the error code registry
- Config, secret, network, and volume removal from detail pages with confirmation dialogs
- Node role change (promote/demote) with radio card selector and quorum impact warnings
- Node removal with type-to-confirm hostname dialog (only available for down nodes)
- Stack removal with type-to-confirm dialog (removes all services, configs, secrets, and networks)
- Service mounts editor with card-based UI supporting all Docker mount types (bind, volume, tmpfs, npipe, cluster, image)
- Service config, secret, and network attachment editors (add/remove references with searchable dropdowns)
- Container configuration editors: command, runtime options, capabilities, extra hosts, DNS settings
- Browsable pages for service sub-resource endpoints (env, placement, ports, policies, log driver, mounts, attachments)
- Docker documentation help links on all service editor fields
- Deployment diff and recent activity shown side by side on wide screens

### Improved
- Detail pages use SSE event payloads for instant sub-resource updates instead of refetching (10 → 2 HTTP requests per event on service pages)
- Health check timeline is keyboard-accessible and scrollable on small screens
- Combobox dropdowns show resource IDs as subtitles for disambiguation
- Editor sections show configured values only, with empty states and edit hints

### Fixed
- Config and secret editors auto-filling incorrect target paths for stack-namespaced resources
- Escape key in combobox dropdowns closing the parent editor instead of just the dropdown

## [0.7.0] - 2026-03-21

### Added
- Operations level setting to restrict write operations by danger tier
- Scale, update image, rollback, and restart actions on the service detail page
- Drain, activate, and pause node availability controls on the node detail page
- Force Remove button on task detail page to kill a task and its backing container
- Last deployment diff on service detail page showing what changed in the most recent service update
- Inline environment variable editor on service detail page (add, edit, remove variables)
- Inline node label editor on node detail page (add, edit, remove labels)
- Inline service label editor on service detail page
- Service resource limits editor on service detail page (CPU and memory limits/reservations)
- Service mode switching (replicated/global) and endpoint mode editing (VIP/DNS-RR)
- Write actions accessible via the command palette (Cmd+K)
- Cluster capacity API endpoint for resource slider bounds
- PromQL metrics query console with autocompletion for metric names, functions, and label values
- Segment-prefix fuzzy matching in global search and PromQL autocompletion
- Copy-to-clipboard buttons on container configuration and health check values
- Docker Swarm template expressions (such as `{{.Node.Hostname}}`) rendered as human-readable badges
- Resizable log viewer (drag bottom edge, double-click to reset)
- Fullscreen log viewer via browser Fullscreen API
- Service placement, ports, update/rollback policy, and log driver read and write endpoints
- Health check editor on service detail page (view and edit health check configuration)
- Interactive health check timeline on task detail page
- Delete service button on service detail page
- Config file auto-discovery from standard locations
- Mobile-responsive layout (navigation, topology, log viewer, search palette, charts)
- Replica doughnut chart on service detail page

### Improved
- All mutation forms use polished shadcn/ui components (popovers, confirmation dialogs, styled dropdowns)
- Resource limits editor shows sliders with cluster-aware bounds and accepts memory in megabytes
- Scale replica input has increment/decrement buttons
- Destructive actions use styled confirmation dialogs instead of browser-native confirms
- Native title attributes replaced with proper tooltips across service detail page
- Activity feed on detail pages no longer shows redundant resource type badge
- Activity feed uses stack-prefix rendering for resource names

### Fixed
- Log viewer appearing empty on service and task detail pages until user interaction
- Node metrics showing when node-exporter targets aren't available
- Duplicate labels section on service detail page
- Long values overflowing in last deployment section
- Command palette showing write actions above the configured operations level

## [0.6.0] - 2026-03-16

### Added
- Pluggable authentication with five providers: anonymous, OIDC, Tailscale, mTLS client certificates, and trusted proxy headers
- Identity display in the navigation bar when authentication is enabled
- `_FILE` suffix support for secret environment variables (for Docker Swarm secrets)

### Security
- Session cookies use `__Host-` prefix for browser-enforced security constraints

## [0.5.0] - 2026-03-16

### Added
- Tasks list page (the "Failed Tasks" card on the homepage no longer returns 404)
- Click-to-isolate on chart legend items in stack drill-down charts
- Keyboard shortcut tooltips on navigation links (hover to discover shortcuts)

### Changed
- Lazy-load all pages and split heavy vendor libraries (Chart.js, topology) into separate chunks—initial load reduced from 2.3 MB to ~360 KB

### Fixed
- Log viewer "has more" indicator inaccurate when filtering by stream (stdout/stderr)
- List page item counts not updating on real-time add/remove events
- Race condition when multiple metrics streams connect simultaneously
- Stale fetch requests not canceled when navigating away from detail/search pages
- Ghost stacks appearing after all services removed but configs/secrets remain
- Metrics charts not reconnecting SSE stream after tab becomes visible again

## [0.4.0] - 2026-03-16

### Added
- Live-updating charts via SSE streaming (no more manual refresh for recent time ranges)
- Range picker with quick presets and custom date-time selection

### Changed
- Replace auto-refresh toggle with streaming play/pause control

### Fixed
- Click-to-isolate and double-click drill-down racing on chart interactions
- Doughnut chart sizing on homepage

## [0.3.0] - 2026-03-16

### Added
- TOML config file and command-line flags as alternative to environment variables
- `healthcheck` subcommand for container health checks

### Fixed
- Simplify disk usage chart to single ring (two-ring layout was confusing; reclaimable info moved to tooltip)

## [0.2.0] - 2026-03-15

### Added
- Migrate from uPlot to Chart.js for all charting
- CVD-safe color palette with theme integration
- Interactive charts: linked crosshairs, click-to-isolate, brush-to-zoom
- Custom date-time range picker with quick presets
- Stacked area toggle on time series charts
- Stack-based drill-down on cluster overview
- Resource allocation bar chart on service detail
- Mock chart data in dev mode when Prometheus is unavailable

### Fixed
- Search input no longer loses focus on refetch
- List pages no longer flash loading skeleton on search/sort

## [0.1.11] - 2026-03-14

### Fixed
- Log fetches for multi-replica services reduced from about 10 seconds to about 2 (idle timeout on stream parser)

## [0.1.10] - 2026-03-14

### Fixed
- Frontend test failures after monitoring status hook was added

## [0.1.9] - 2026-03-14

### Fixed
- Stale browser-cached responses (added Cache-Control headers)
- Data race in cross-reference lookups under concurrent reads
- Dockerfile health check path
- Docker Compose external network name for monitoring stack

### Security
- Bump undici

## [0.1.8] - 2026-03-14

### Added
- Per-task CPU/memory sparklines on service, node, and task pages
- Metrics panels on cluster overview, node list, and service list
- Leader badge, availability, and address columns on node table
- Ports column on service table

### Fixed
- Resource gauges crash when no resource limit is set
- Page titles all showed "frontend" instead of the resource name
- Tables clip on small viewports

## [0.1.7] - 2026-03-13

### Fixed
- Double borders on deploy config detail panels
- Ghost stacks appearing from orphaned volumes

## [0.1.6] - 2026-03-13

### Fixed
- Duplicate edges in logical topology view
- Overlapping nodes in physical topology view
- Stack health incorrectly counting historical task failures

## [0.1.5] - 2026-03-13

### Fixed
- Node metrics showing wrong values in overlay network deployments (now resolves by hostname instead of IP)

## [0.1.4] - 2026-03-13

### Fixed
- Service replica counts inflated by shutdown tasks
- Log viewer making unnecessary polling requests
- Spurious task change events in activity feed
- Running tasks not sorting first in task lists
- Task detail service link not clickable

## [0.1.3] - 2026-03-13

### Fixed
- Dockerfile naming and base image updates for release builds

## [0.1.2] - 2026-03-13

### Fixed
- SSE test data race under `-race` flag
- CI compatibility with Go 1.26 and latest GitHub Actions

### Security
- Bump hono to 4.12.7 (prototype pollution CVE)

## [0.1.1] - 2026-03-13

### Fixed
- CI and dependency fixes

## [0.1.0] - 2026-03-12

### Added
- Per-resource SSE streaming on all list and detail endpoints
- OpenAPI 3.1 spec with Scalar API playground at `/api`
- JSON-LD metadata and RFC 9457 error responses
- Content negotiation via `Accept` header or `.json`/`.html` extension
- ETag conditional caching with 304 Not Modified
- Global cross-resource search with `Cmd+K` command palette
- Network topology view (logical and physical)
- Stack detail pages with member resources
- Log viewer with live streaming, regex search, JSON formatting
- Monitoring auto-detection (Prometheus, cAdvisor, node-exporter)
- Node resource gauges and service/stack metrics panels
- Disk snapshot persistence for instant dashboard on restart
- Expression-based filtering on all list endpoints
- Virtual scrolling for large tables
- Activity feed with recent resource changes
- Multi-platform Docker images (amd64, arm64) with SBOM and provenance

### Security
- Secret values never exposed in API responses
- Prometheus proxy restricted to query endpoints
- Connection limits: 256 SSE clients, 128 concurrent log streams
