Refresh the UI from Docker events instead of polling (0.49.0)
F16 — /ws/events was implemented and nothing consumed it, while thirty polling intervals across the pages asked for state that only changes when Docker does something. The endpoint was the answer; it just was not usable as it stood, so this is three fixes and a client, not a wiring job. The endpoint forwarded the whole firehose. Three exec_* events fire per web terminal session and top/attach fire whenever anything inspects a container, so a client invalidating on each would have been noisier than the polling it replaces. Now the daemon filters by resource type and the handler drops the actions that say nothing about rendered state — matching on the verb before the colon, since Docker reports these as "exec_create: /bin/sh". It never said *what* changed, so there was nothing to decide which caches to drop. The payload now carries the resource type. And it leaked its reader thread. Cancelling the executor future does not interrupt a thread already inside a blocking read; closing the underlying CancellableStream is what does. Every page load left one behind holding a socket open. A test asserts the close, because this is invisible until the process has been up for a week. Client side, useDockerEvents holds one connection for the session and maps resource types to query keys. Bursts are coalesced over 300ms — a ten-service compose up emits dozens of events in a second, and refetching per event would reintroduce exactly the load being removed. Reconnects back off to 30s, and any close reconnects including 4401, since the access token is short-lived and gets refreshed out from under the socket. Intervals drop from the mechanism to the safety net: 5s becomes 30-60s. Two deliberately stay fast. Live CPU/memory drifts continuously with no event to announce it, and that one is served from the 4s server-side cache added in 0.47.0, so it costs one sample per interval regardless of how many tabs are open. The audit feed polls because its entries come from people, not Docker. Net effect is both cheaper and faster: no fixed floor of requests per second against the daemon, and a stack that finishes starting shows up immediately rather than up to five seconds later. 23 new tests (758 total), driven against a fake daemon. Both nets were checked by reverting the fix: dropping the filter fails one, dropping the stream close fails the leak test. Not covered: the hook itself has no test — there is no frontend test runner yet. Its contract with the backend is tested; its own behaviour is only typechecked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dk43rmEeRfYi5wsLDbmfyG
This commit is contained in:
@@ -13,6 +13,24 @@ as intuitive as Dockge, as capable as Portainer for Compose workflows.
|
||||
> (Auto-update) + Phase 23 (Secrets & configs) + Phase 24 (Design System v2)
|
||||
> complete.
|
||||
|
||||
## Upgrading to 0.49.0 — nothing to do
|
||||
|
||||
The UI now refreshes when Docker changes instead of asking every few seconds.
|
||||
One WebSocket (`/ws/events`) carries container, image, network and volume
|
||||
events; the client drops the matching query caches and re-renders. The polling
|
||||
intervals stay behind it as a safety net for a dropped socket, at 30–60s rather
|
||||
than 5s.
|
||||
|
||||
Live CPU and memory keep their fast poll on purpose — usage drifts continuously
|
||||
and Docker emits no event for it. That request is served from a 4-second
|
||||
server-side cache, so it costs one sample per interval no matter how many tabs
|
||||
are open.
|
||||
|
||||
The endpoint existed before this release but nothing used it, and it was not
|
||||
usable as it stood: it forwarded every event including three `exec_*` per web
|
||||
terminal session, never said *what* had changed, and leaked its reader thread on
|
||||
every disconnect. All three are fixed.
|
||||
|
||||
## Upgrading to 0.48.0 — remote hosts are gone
|
||||
|
||||
The multi-host feature (the `stackpilot-agent` sidecar and everything that
|
||||
@@ -134,6 +152,11 @@ it is what your saved destination credentials are encrypted with.
|
||||
holds across workers and across a restart) and a second one gets `409` while
|
||||
it is held; auto-update skips a stack somebody is already deploying. Locks
|
||||
carry an expiry, so a worker killed mid-deploy does not strand a stack.
|
||||
- **Event-driven UI** — a single `/ws/events` connection carries Docker's own
|
||||
container / image / network / volume events; the client drops the matching
|
||||
caches so pages refresh the moment something changes, instead of every page
|
||||
polling on a timer. The intervals remain as a slow fallback. Live CPU and
|
||||
memory still poll, because usage drifts with no event to announce it.
|
||||
- **Real-time logs** — streamed over WebSocket, color-coded per service.
|
||||
- **Live deploy console** — deploying from the editor streams `compose up`
|
||||
output (image pulls, container creation) over a WebSocket in real time instead
|
||||
@@ -509,7 +532,7 @@ Same three commands the CI runs — `build-and-push` only starts once they pass.
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements-dev.txt
|
||||
pytest # 735 tests, no Docker daemon needed
|
||||
pytest # 758 tests, no Docker daemon needed
|
||||
ruff check .
|
||||
cd ../frontend && npx tsc --noEmit -p tsconfig.json
|
||||
```
|
||||
@@ -533,6 +556,13 @@ HTTP. `tests/test_schema_migration.py` builds a database with the *old* user
|
||||
table and asserts the added column is backfilled rather than left NULL, which is
|
||||
what would otherwise have signed out every user on every install.
|
||||
|
||||
`tests/test_docker_events.py` drives the event stream against a fake daemon: that
|
||||
`exec_*` noise is dropped before it reaches the client, that the payload names
|
||||
the resource so the client knows what to invalidate, and that the stream is
|
||||
closed on disconnect — cancelling the executor future does not interrupt a
|
||||
thread already inside a blocking read, so without that close every page load
|
||||
leaked one.
|
||||
|
||||
`tests/test_stack_locking.py` and `tests/test_runtime_state.py` cover the state
|
||||
that moved into the database: that a busy stack answers 409 without ever
|
||||
reaching Docker, that an expired lock is taken over rather than stranding the
|
||||
|
||||
Reference in New Issue
Block a user