Difficulty: Advanced · You'll need: Go, TypeScript · Size: ~200 lines · Candidate for v0.2.0 — filed for the decision
What's going on
Container state in the dashboard is refreshed by polling, not by push:
apps/web/src/hooks/useContainers.ts → refetchInterval: 20_000
- each poll calls
GET /hosts/:id/containers → AgentsGateway.requestContainerList → a fresh container.list over the socket → the agent runs docker ps and replies
Why it matters
- State is stale for up to 20 seconds. A container that crash-loops, OOMs or exits shows as
running until the next poll. For a monitoring product that is the number the operator most needs to be current.
- It costs a
docker ps per host per 20s, forever, whether or not anything changed and whether or not anyone is looking. With N hosts open in a dashboard tab, that is N calls every 20 seconds indefinitely.
- The platform already holds a live bidirectional socket to every host. Polling over a push-capable channel is the thing the WebSocket decision in
docs/architecture.md was made to avoid — the same argument the "Why WebSocket?" table uses to reject agent-polls-REST.
What Docker offers
GET /events is a long-lived stream of container lifecycle events — start, die, stop, pause, unpause, destroy, health_status. The agent already knows how to consume a long-lived Docker stream: internal/logs/stream.go does exactly this for logs, including a decode goroutine, batching at 50 entries / 200ms, and idempotent teardown.
Sketch
- Agent: subscribe to Docker events at startup, filtered to
type=container. Debounce and batch, following logs/stream.go. Push a new container.event (or a re-sent container.listed) when the set changes.
- Protocol: a new message type. Per design commitment 2 in
docs/roadmap.md, new capability arrives as new type values — container.listed stays untouched, so an older agent that only answers polls keeps working.
- Server: update
ContainerInventoryService on receipt. Push to the browser over the existing SSE machinery, or keep polling the server's now-fresh cache — the second is a much smaller change and removes the per-poll socket round trip and docker ps.
- Web: drop or lengthen
refetchInterval once the cache is authoritative.
Decisions to settle first
- New message, or re-send
container.listed? Re-sending the full list is simpler and idempotent; a delta event is cheaper on a host with many containers.
- How far does the push go? Agent→server is the valuable half. Server→browser can stay polled at first, since the server's cache would then be current.
- Does polling stay as a fallback? An agent that fails to open the events stream should still be listable. Keeping
container.list as the fallback path is probably non-negotiable.
- Event storms.
docker compose up on a 40-container stack emits hundreds of events in a second. Debouncing is required, not optional.
Why this is filed as a decision rather than a task
Everything container-related in v0.1.0 works; this changes how it stays current. It is the largest remaining gap in container management, but it is a design change to the refresh model, not a missing feature — so it likely belongs to v0.2.0. Filed so the trade-off is recorded rather than rediscovered.
Done when
Difficulty: Advanced · You'll need: Go, TypeScript · Size: ~200 lines · Candidate for v0.2.0 — filed for the decision
What's going on
Container state in the dashboard is refreshed by polling, not by push:
apps/web/src/hooks/useContainers.ts→refetchInterval: 20_000GET /hosts/:id/containers→AgentsGateway.requestContainerList→ a freshcontainer.listover the socket → the agent runsdocker psand repliesWhy it matters
runninguntil the next poll. For a monitoring product that is the number the operator most needs to be current.docker psper host per 20s, forever, whether or not anything changed and whether or not anyone is looking. With N hosts open in a dashboard tab, that is N calls every 20 seconds indefinitely.docs/architecture.mdwas made to avoid — the same argument the "Why WebSocket?" table uses to reject agent-polls-REST.What Docker offers
GET /eventsis a long-lived stream of container lifecycle events —start,die,stop,pause,unpause,destroy,health_status. The agent already knows how to consume a long-lived Docker stream:internal/logs/stream.godoes exactly this for logs, including a decode goroutine, batching at 50 entries / 200ms, and idempotent teardown.Sketch
type=container. Debounce and batch, followinglogs/stream.go. Push a newcontainer.event(or a re-sentcontainer.listed) when the set changes.docs/roadmap.md, new capability arrives as newtypevalues —container.listedstays untouched, so an older agent that only answers polls keeps working.ContainerInventoryServiceon receipt. Push to the browser over the existing SSE machinery, or keep polling the server's now-fresh cache — the second is a much smaller change and removes the per-poll socket round trip anddocker ps.refetchIntervalonce the cache is authoritative.Decisions to settle first
container.listed? Re-sending the full list is simpler and idempotent; a delta event is cheaper on a host with many containers.container.listas the fallback path is probably non-negotiable.docker compose upon a 40-container stack emits hundreds of events in a second. Debouncing is required, not optional.Why this is filed as a decision rather than a task
Everything container-related in
v0.1.0works; this changes how it stays current. It is the largest remaining gap in container management, but it is a design change to the refresh model, not a missing feature — so it likely belongs tov0.2.0. Filed so the trade-off is recorded rather than rediscovered.Done when
v0.1.0orv0.2.0)docker pspoll is gone