Changelog
All notable changes to Cetacean are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[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 /historytakessearchand abeforecursor, and a full page links the next one withrel="next"server.version_checkturns off the call toapi.github.comfor the latest Docker Engine release- The nav bar links the plugin list, and
gpopens 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:
Allowlists a resource's own methods, not its sub-resources'. A detail response links each sub-resource you may write withrel="edit", andAccept-Patchmoves to the sub-resources that take aPATCH. API clients reading writes off a parent'sAllowmust read the links - Breaking:
?filter=refuses a field the resource doesn't have, or a comparison of mismatched types, with400(FLT002). Taskexit_codeis a number,0until the task ends: writeexit_code != 0, not"0" - Breaking: a failed
POST /-/resyncanswers with problem details:504(ENG005) when Docker didn't answer in time, otherwise502(ENG006) naming what each list answered. Clients must readdetail, noterror - A Client ID Metadata Document is cached as its response's
Cache-ControlorExpiresallows, 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
/, likevieux/sshfs, resolves behindserver.base_pathand with a.jsonsuffix - 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
405withAllow, and an unknown or disabled/-/endpoint404, 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/readof an unknown ID answers-32602, or-32002on 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/cancelledwith 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 /pluginsandPOST /plugins/{name}/upgradeneed theprivilegesthe check returned, and answer409if the plugin now asks for others. API clients must send the reviewed list - Breaking: writes with no
GETat their own path, such asPUT /services/{id}/scaleand the/swarmupdates, refuse anyIf-Matchwith412instead 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
localvolume, 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_capabilityoracl.headers_aclgave you at consent, so API and MCP clients get the access your browser session has GET /auth/whoamiaccepts 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
certmode 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/listrequest - 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
Allowheader on a task and on a node with no hostname offers exactly the writes their grants permit GET /api/asyncapi,/api/context.jsonld,/metrics/statusand/auth/whoamianswer 406 to a request that accepts none of their types, instead of sending JSON anyway- Fixed a list request whose
Range: itemsheader ends at the largest integer answering 500 instead of its first page - A
304 Not Modifiedon 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 /-/resyncsometimes 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.hostnaming 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}/resourcesanswers an unset requirement with{}, as theGETdoes, instead ofnullPATCH /services/{id}/mountsanswers a body that isn't JSON withAPI008, 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_capabilityreads header-mode grants from a JSON object keyed by capability, so grants can come from the tailnet policy throughtailscale serve'sTailscale-App-Capabilitiesheader- A History page at
/historylists the cluster's recent changes, filterable by type and name, with a CSV download of the full log docker.tls.ca,docker.tls.certanddocker.tls.keyconnect to atcp://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
.yamlto its URL. Secrets and configs are referenced, never exported GET /api/asyncapidescribes 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.yamland/api/asyncapi.yamlor by negotiating onAccept - Any list can be downloaded as CSVâadd
.csvto the URL or ask fortext/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 itGET /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_webredirects to its ID-addressed URL. The redirect keeps the method and body, so writes work by name withcurl -L - Write operations accept an optional
If-Matchheader and refuse with 412 if the resource changed since you read it Prefer: waitholds a service write open until the cluster settles, andPrefer: respond-asyncacknowledges 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_urlis 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_headersnames which forwarding headers a trusted proxy writesâx-forwardedby default, orforwardedfor 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 /-/healthreports whether Cetacean is still tracking the cluster, and/-/metricssays the same for alerting. The dashboard marks itself stale instead of showing a frozen cluster as a live onePOST /-/resyncandGET /swarm/pluginsappear 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_tokensto offer it oauth.token_operations_levelholds 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.txtlists the site, and/openapi.jsondescribes what it serves - A stack's page and a service's Traefik labels are each drawn as a graph
GET /nodes/{id}/drain-impactsays 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 MCPinstead of a URL - The Docker Hub image carries the same build provenance as the GHCR one, so
gh attestation verifyworks 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, andGET /metrics/labelsneedreadon the bare resource*; anyone else gets403and the dashboard leaves out its charts. MCPget_metricsis 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.hostrefuses startup instead of failing on DNS; it never connected. Forward the engine's socket over SSH, or usetcp://withdocker.tls - Breaking: MCP's
remove_taskneeds operations level 3, matchingDELETE /tasks/{id}; it was available from level 1 - Breaking: the OAuth authorization server is opt-inâset
oauth.enabled. Under any auth mode butnone, MCP needs it or the active mode named inmcp.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 andCETACEAN_OAUTH_*variables:issuer,signing_key, the three TTLs,require_resource_indicator, thedcr_*trio andcimd_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 thantrue,false,1or0, or an unknownlogging.levelorlogging.formatrefuses startup. Check your environment for typos before upgrading - Breaking:
auth.headers.trusted_proxiesis goneâuseserver.trusted_proxies, which headers mode already required - Breaking:
mcp.oauth.auth_bypassis nowmcp.auth_bypass, and accepts onlycert,headers, andtailscaleâa listed mode authenticates/mcpon its own, sooauth.enabledcan stay off - Breaking:
/mcp's protected resource metadata moved to/.well-known/oauth-protected-resource/mcp, so one document describes one resource. A client followingresource_metadatafrom the 401 is unaffected; one that hardcoded the root path isn't - Breaking:
mcp.operations_levelnarrowsserver.operations_levelinstead 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.jsonunderstorage.data_dir. The formermcp-tokens.jsonisn't readâdelete it, and every client authorizes once more - Breaking: the
refresh_tokengrant at/oauth/tokenrequiresclient_id; a request without it is refused withinvalid_request - Clients that registered dynamically stay registered across a restart, instead of having to register again
/oauth/registerrejects a client name over 256 bytes, more than 10 redirect URIs, or a redirect URI over 2048 bytes/oauth/registeraccepts onlytoken_endpoint_auth_method: none; any other value is refused rather than stored and treated as public- Upgrade note:
X-Forwarded-ProtoandX-Forwarded-Hostare honored only from an address inserver.trusted_proxies. Behind a proxy without it set, absolute URLs now name the internal addressâsetserver.public_urlor 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
/apiis six months newer, and now follows Scalar releases automatically - Access tokens follow the RFC 9068
at+jwtprofile. Clients holding an older token refresh automatically - The Cetacean API description moved to
/api/openapi.yaml;/openapi.jsonnow describes the documentation site itself - Breaking:
CETACEAN_OAUTH_SIGNING_KEYis now a root secret both keys derive from, and must be 32 bytes of hex or base64âgenerate one withopenssl 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
kidchanges. 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
Acceptheader it doesn't recognize - Breaking:
server.operations_leveldefaults to0, read-only. A deployment that never set it could perform operational writes, and onauth.mode=nonecould do so unauthenticatedâset it to1to keep that - Breaking: a refused request answers
403rather than401undercert,tailscale, andheadersâno challenge can ask for the credential those modes read - Signing in from an address ending in
.atom,.csv, or.jsonreturns to that resource's page, not the file PATCH /services/{id}/secretsand/configstake the name, and the default mount path, from the secret or config the ID names;secretNameandconfigNamein the body are ignored- MCP's
cetacean://clusterandget_cluster_statusneed at least one grant, asGET /clusterdoes - 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}/modeis unaffected
Fixed
- tsnet mode serves the app over HTTPS with the node's certificate, and
/-/metricsonserver.listen_addrfor in-swarm scrapers. Before upgrading, unsettls.certandtls.key, and setserver.public_urlifoauth.enabledis on GET /swarmand the Swarm page reveal the join tokens only at operations level 3, to callers with write onswarm: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 answers409 API015 - Creating a config or secret through the API checks the write grant on its name, so a grant like
secret:team-*can createteam-db - A service port with no
TargetPortis refused with400 SVC020instead of being published to nowhere cetacean healthcheckprobes the server its config file, flags and TLS settings describe, including a listen host and tsnet mode withserver.base_path; flags may precede it, and unknown subcommands are refused- With
server.cors.originsset to*,/mcprefuses a cross-site browser origin, as the docs say, instead of accepting every origin /mcprefuses a request body over 1 MiB with413, and more than 64 opensubscriptions/listenstreams with429andRetry-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 with400 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 --helpexits 0 and states the operations level default as 0- Startup warns when MCP is enabled without TLS outside auth mode
noneandserver.public_urlisn'thttps GET /swarm/unlock-keyis sent withCache-Control: no-storeand varies by identity, so no cache keeps the key- Creating a config or secret answers
201with a JSONContent-Typeand is logged as201;/-/readysends its503as 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-usagereuses 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.thresholdsout of order - Startup warns when
auth.modeisnonewith 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/net0.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 /-/resynccalls 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_dircouldn'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 /eventsapplies?types=and, on reconnect withLast-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 onGET /events, its replay, and MCP subscriptions - Shutdown ends open
/eventsand resource event streams first instead of waiting out the grace period and logging an error Acceptmedia types and parameters, and theitemsrange 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
Origineven when they grant nothing - In headers mode, a request carrying the subject, name, email, groups, secret or
acl.headers_aclheader 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
409with 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
forceoption 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
LOG007or an empty live tail, and a service's own--log-optlabels no longer leave the raw label prefix in every message - The activity feed,
GET /historyand the history feeds name tasks asservice.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 underserver.base_path - The API explorer's policy no longer allows inline scripts, and a caller's
traceparentcan no longer switch off tracing of its own requests - An OAuth client evicted at
oauth.dcr_max_clientsis 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 MCPget_metricssay only that; the cause is logged GET /metricsandGET /metrics/labelsanswer while Docker is unreachablemanager-has-workloadsis raised only when a ready, active worker could take the manager's tasks, andget_recommendationsno longer offersupdate_nodeas its fix- JSON log reads are capped like log streams (
429 LOG009), and aPrefer: waitpast its cap is answered asrespond-asyncwith aRetry-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 withCache-Control: private, since it names the host the request carried - A conditional request for
/servicesor/tasksis answered with304before the list is built, as the other lists already were - Grants under
acl.tailscale_capabilitynow apply - The OAuth consent page lists every redirect URI the client registers
- The API explorer at
/apino longer tries to load hosted fonts or send telemetry - Setting
com.docker.stack.namespaceon 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
/apiloads underserver.base_path, and the OpenAPI document'sserversnames the base path - The OpenAPI document describes
Rangepagination 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 anerrorslist Allowno longer offersPOSTon a single config or secret,PUTon 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
flappingand counts as unhealthy in MCP'sget_cluster_status, the overview page and search, instead ofrunningwhenever a replica is briefly up - A service's activity,
GET /historyand MCP'sget_eventsinclude its tasks' changes, so a crash-looping service no longer shows an empty timeline - Job-mode services read
completedonce their run finishes instead ofrunningforever,Prefer: waitand MCP'swatchwait 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 /searchand MCP'sfindcount only matches the caller may read, so a count no longer reveals resources the policy hides- History feeds,
GET /history, andcetacean://historyfill 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-Matchupdate to a service, node, config or secret is refused with412if 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 declaresclient_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_methodother thannoneis refused instead of treated as a public client, as the draft requires - A stream refused at its connection cap asks for a different
Retry-Aftereach 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 /mcpaccepts clients on MCP revision2025-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-28is told which revisions are supported, without the ones it would be refused on - Addressing a task as
web.1reaches 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 /historyand the MCPget_eventstool 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
audis an array, or whosetypdiffers only in letter case, is acceptedâboth are conformant shapes that were refused - The authorization server advertises that it sends
isson authorization responses, so a client actually enforces the mix-up check the responses already carry - A token request missing
grant_type,codeorrefresh_tokenis refused withinvalid_requestrather thaninvalid_grant, which told clients to discard a working grant - A repeated RFC 8707
resourceparameter is refused withinvalid_targetinstead 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
filteris reported as an error again on a request carryingIf-None-Match: * - The authorization response redirects with
303 See Otherrather than302 Found - A redirect URI carrying a fragment is refused at registration
- A write carrying
If-Matchagainst a resource that no longer exists answers404, not412â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
404with a problem document rather than200and the dashboardâon a write, and on a read from a client that said it can't use a web page - A format an
Acceptheader rules out with;q=0is refused with406instead of served anyway /favicon.icoand the dashboard's other static files are no longer refused with406when 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.htmlopens 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
409naming the conflict, instead of a bare500 Docker Engine Error - A
PATCHto 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 /-/resyncrequires authentication but not an operations level, so a read-only deployment keeps its refresh button- A task no longer sits at
startingfor 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.endpointtakes 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
ETagandCache-Control, so a client polling their cursors can revalidate - JSON Feed responses are labeled
application/feed+jsonrather 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_urlto 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/errorsrenders instead of showing "Something went wrong" - The API documentation, the playground script, the JSON-LD context and the attribution documents are cacheable
/oauth/authorizeno longer answers cross-origin requests, whateverserver.cors.originsallows, so a page on an allowed origin can't read the consent form
[0.14.0] - 2026-09-10
Added
server.public_urlsets 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 cetaceanstands alone. Add-c compose.prometheus.yamlto 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_logsgained thefatallevel - 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_urlwhen it's set, rather than from request headers make buildworks 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, andaclkeys that don't exist, so anyone uncommenting one got no grants and no warning. The working keys areoidc_claim,tailscale_capability, andheaders_aclunder[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 findanddescribereturn 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=0always 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_FILEreads 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
findtool, with a newdescribefor detail; raw records are still available on request - Breaking: the eleven MCP tools that edited a service or node specification are now
update_serviceandupdate_node, naming the part they change in asectionargument.update_node_labelsstays separate because it needs a lower operations level - Breaking:
update_serviceno longer acceptssecrets,configsormountsas asection. 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
searchtool returnsresults/counts/total, matching the HTTP search response - Breaking:
/topology/networksand/topology/placementare removed./topologyhas replaced them since 0.12.0 and both paths answer410 Gonenaming it - Breaking:
CETACEAN_MCP_SESSION_IDLE_TTLandCETACEAN_MCP_MAX_SESSIONSare 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 cetaceanworks 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
/topologyreturns 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
commandand its arguments asargs, 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_KEYto 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 byCETACEAN_MCP_MAX_TASK_TTL CETACEAN_MCP_CONSENT_TTL=0works as documented; setting it stopped Cetacean from starting- Turning off
CETACEAN_MCP_CIMD_ENABLEDtakes 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-Idheader 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_ISSUERsets 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
Originwith 403, a DNS-rebinding defense. The allowlist isCETACEAN_CORS_ORIGINS; non-browser clients send noOriginand 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
undefinedfor settings Docker omits at their default - Plugin type displays as
docker.volumedriver/1.0rather 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_BYPASStakes effect, so MCP clients can reach/mcpon upstream identity such as mTLS without a bearer token- MCP
tools/listhides 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
searchrejects an empty query instead of returning every cached resource, and no longer advertises atypesparameter it ignored update_service_imagerejects 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_typesandresponse_typesrather 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
updatingwhile a rollback is in progress, not only a forward update - Website dependencies can be installed again; an unresolvable Astro peer conflict made every
npm installfail
[0.11.2] - 2026-05-20
Security
- Bumped
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttpto 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 criticalprotobufjsarbitrary-code-execution advisory (GHSA-xq3m-2v4x-88gg) and the bundleddompurify/astroadvisories
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+xmlheader or.atomURL 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 /-/resyncendpoint 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 undefinederror) - 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-srcCSP directive now that no external images are referenced - Filter expressions like
exit_code != "0"no longer match running tasks (Docker reports-1mid-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
-1while 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_totalmetric 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,?forceon node and volume deletion, andapplication/merge-patch+jsonsupport on env and label PATCH endpoints - Removed dead
GET /swarm/pluginsalias route - Empty cross-reference and collection fields now serialize as
[]instead ofnullon 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
Allowresponse header indicating available methods per resourceâthe dashboard uses this to show or hide write controls per resource and per userAccept-Patchresponse header advertising supported patch formats per resource (RFC 5789)Prefer: return=minimalsupport on all write endpoints (RFC 7240)Last-Modified/If-Modified-Sinceconditional requests on resource detail endpointsStrict-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-49returns206 Partial ContentwithContent-Range) - Infinite scroll on all resource list pagesâitems load automatically as you scroll down
Deprecated
CETACEAN_AUTH_HEADERS_TRUSTED_PROXIESâuseCETACEAN_TRUSTED_PROXIESinstead; 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-IDheader renamed toRequest-Idper RFC 6648 (deprecation ofX-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_METRICStoggle to disable the self-metrics endpoint (enabled by default)CETACEAN_RECOMMENDATIONStoggle 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/) viaCETACEAN_BASE_PATHenvironment variable,--base-pathflag, orbase_pathTOML config - Recommendation engine:
/recommendationspage 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
_FILEsuffix 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
healthchecksubcommand 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
-raceflag - 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
Acceptheader or.json/.htmlextension - ETag conditional caching with 304 Not Modified
- Global cross-resource search with
Cmd+Kcommand 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