feat(master): Ledger UI for Services and Agents (#18) #29

Merged
shcizo merged 6 commits from feat/ledger-ui-services-agents-18 into main 2026-10-13 09:20:22 +00:00
Owner

Summary

  • Rebuilds the master's Services and Agents pages per the "Ledger" design: sidebar shell with live nav counts, machine filter and Live indicator; dark/light theme (follows the OS, toggle overrides); embedded Schibsted Grotesk and JetBrains Mono (fontsource 5.3.0, OFL).
  • Services: Swarm grouped per cluster with its machines under it, offline banner ("Agent offline since HH:MM", browser-local), Simple Icons derived from the image ref (letter circle fallback), existing badges and mode switch restyled, client-side search, machine filter and Icon / Icon + text / Text toggle.
  • Agents: Online / Offline / Never connected / Revoked, master version in the header with outdated agent versions marked, revoke via popover instead of confirm(), "Add agent" as a three-step panel that completes when the agent says hello (SSE agents).
  • Store: migration 0005_agent_cluster.sql persists each agent's swarm_cluster_id (needed to list a cluster's managers and its offline state after a master restart).
  • SSE keepalive is now a named ping event so the Live indicator also shows the connection is alive. revokeAgent now publishes agents.

Closes #18

Notes

  • Workers never report a cluster id (Docker only sets Swarm.Cluster on managers), so their Swarm role shows "—". Fixing that needs a new hello field.
  • Master binary grows by about 2.16 MB, almost all Simple Icons 16.35.0 path data (gzipped JSON, regenerated with go generate ./internal/master/web/icons). The agent binary does not import it.
  • Jobs, Job and Service detail pages only get the new shell; their own artboards are out of scope, as are Update buttons, Update all, header stats and the drift badge.
  • Unchanged: CSRF on every POST, Cache-Control: no-store on the token response, 401 + HX-Redirect without a session, no template.HTML on agent- or user-controlled values, no Node build step.

Test plan

  • go test ./..., go test -race ./internal/master/...
  • go test -tags integration ./internal/master/store/ against Postgres 17
  • docker build
  • Browser check on fake data: both themes, 1440 px and 390 px, add-agent flow through connect
  • Services grouping and icons against real stacks and a real Swarm cluster
  • "Agent offline since" after stopping an agent
  • Theme toggle, search and machine filter across reloads and mode changes
  • Add agent end to end with a real agent, Copy buttons
  • Phone at 390 px

https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ

## Summary - Rebuilds the master's **Services** and **Agents** pages per the "Ledger" design: sidebar shell with live nav counts, machine filter and Live indicator; dark/light theme (follows the OS, toggle overrides); embedded Schibsted Grotesk and JetBrains Mono (fontsource 5.3.0, OFL). - Services: Swarm grouped per cluster with its machines under it, offline banner ("Agent offline since HH:MM", browser-local), Simple Icons derived from the image ref (letter circle fallback), existing badges and mode switch restyled, client-side search, machine filter and Icon / Icon + text / Text toggle. - Agents: Online / Offline / Never connected / Revoked, master version in the header with outdated agent versions marked, revoke via popover instead of `confirm()`, "Add agent" as a three-step panel that completes when the agent says hello (SSE `agents`). - Store: migration `0005_agent_cluster.sql` persists each agent's `swarm_cluster_id` (needed to list a cluster's managers and its offline state after a master restart). - SSE keepalive is now a named `ping` event so the Live indicator also shows the connection is alive. `revokeAgent` now publishes `agents`. Closes #18 ## Notes - Workers never report a cluster id (Docker only sets `Swarm.Cluster` on managers), so their Swarm role shows "—". Fixing that needs a new hello field. - Master binary grows by about 2.16 MB, almost all Simple Icons 16.35.0 path data (gzipped JSON, regenerated with `go generate ./internal/master/web/icons`). The agent binary does not import it. - Jobs, Job and Service detail pages only get the new shell; their own artboards are out of scope, as are Update buttons, Update all, header stats and the drift badge. - Unchanged: CSRF on every POST, `Cache-Control: no-store` on the token response, 401 + `HX-Redirect` without a session, no `template.HTML` on agent- or user-controlled values, no Node build step. ## Test plan - [x] `go test ./...`, `go test -race ./internal/master/...` - [x] `go test -tags integration ./internal/master/store/` against Postgres 17 - [x] `docker build` - [x] Browser check on fake data: both themes, 1440 px and 390 px, add-agent flow through connect - [ ] Services grouping and icons against real stacks and a real Swarm cluster - [ ] "Agent offline since" after stopping an agent - [ ] Theme toggle, search and machine filter across reloads and mode changes - [ ] Add agent end to end with a real agent, Copy buttons - [ ] Phone at 390 px https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ
shcizo added 5 commits 2026-10-13 09:02:36 +00:00
The Ledger UI groups Swarm services under their cluster and lists the
cluster's managers, so the master needs to know which cluster an agent
belongs to. RecordHello now stores hello.SwarmClusterID in the new
agents.swarm_cluster_id column.

Claude-Session: https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ
Icons: simple-icons@16.35.0 (CC0), generated by
go run ./internal/master/web/icons/gen, which downloads and verifies:
https://registry.npmjs.org/simple-icons/-/simple-icons-16.35.0.tgz
20ebead4f6bc93464812a5e260d19fc4d84772e5eeff69a6fbea263ca2441c5f
Output (deterministic): simple-icons.json.gz
a8f3b504cc21207e8d55d50f3724630a470968da69451c731538589c4debc87b

Fonts (@fontsource-variable 5.3.0, latin, SIL OFL 1.1), sha256:
4c8b93f431d462c696e12b9d6a033feb3394d36e66e781357c496b95d8a75e05  schibsted-grotesk-latin-wght-normal.woff2
18be452724bfdc236c074ca94a249a7f41a86752c7d04ab258ce9ed5651f6a7e  jetbrains-mono-latin-wght-normal.woff2
15222412a95d70042e66e79bfb3b985653e0693e4d41703bc131bee9c5aa1aa7  OFL-schibsted-grotesk.txt
403581b69dac5cff4079205e01c6b467e56af449ecbd7247693ddb1baafa005b  OFL-jetbrains-mono.txt
from https://cdn.jsdelivr.net/npm/@fontsource-variable/{schibsted-grotesk,jetbrains-mono}@5.3.0/

Claude-Session: https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ
Restyles the master UI after the Ledger design: a sidebar with live counts,
a machine filter, a Live box and a light/dark toggle; the overview groups
services per Swarm cluster and machine with icons, search and the
Icon / Icon + text / Text switch; the agents page gets a revoke popover and
an add-agent panel with a connect step. Jobs, Job and Service pages get the
new shell and tokens only.

- fonts and Simple Icons are served by the master (no CDN)
- the overview reads agents too, to show offline banners and cluster managers
- revoking an agent also publishes the agents event
- the SSE keepalive is a named event (ping) the Live box counts
- the master passes its build version to the UI so agents on another
  version are marked old

Claude-Session: https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ
- icons/gen writes to the current directory so `go generate
  ./internal/master/web/icons` works (output verified byte-identical)
- share the registry-host check as imageref.IsRegistryHost (with test)
- agents fragment refreshes the "N machines" header count out of band
- ignore the saved machine filter at <=760px where the machine list is hidden
- aria-current on the active machine filter link, aria-pressed on theme toggle
- CLAUDE.md: icons package, SSE ping event, swarm_cluster_id

Claude-Session: https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ
- Move the .agent @container rule after the base .agent rules. A container
  query adds no specificity, so the earlier position always lost and rows
  kept six fixed columns, spilling Revoke buttons onto the open drawer.
- Give .drawer min-width:0 so the compose snippet scrolls inside its <pre>
  instead of widening the drawer past the viewport at 390px.

Claude-Session: https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ
shcizo added 1 commit 2026-10-13 09:18:56 +00:00
Conflicts:
- CLAUDE.md: keep both sets of gotchas (drift/merge from main, SSE ping and
  swarm_cluster_id from this branch).
- internal/imageref/imageref_test.go: keep both TestIsDigest (main) and
  TestIsRegistryHost (this branch).
- internal/master/web/server.go: take main's renderServiceError and make it
  render through s.page, so service error pages get the sidebar nav data.

Also renumber this branch's migration 0005_agent_cluster.sql to
0006_agent_cluster.sql, after main's 0005_drift_merge.sql.

Claude-Session: https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ
shcizo merged commit ec28d1e027 into main 2026-10-13 09:20:21 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: shcizo/package-updater#29