feat(master): Ledger UI for Services and Agents (#18) (#29)
Docker Build / docker (push) Successful in 41s
Docker Build / docker (push) Successful in 41s
## 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
Reviewed-on: #29
This commit was merged in pull request #29.
This commit is contained in:
@@ -1,23 +1,62 @@
|
||||
{{define "layout"}}<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="app">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>{{.Title}} · package-updater</title>
|
||||
<script>
|
||||
// Sets the theme and the display mode before first paint. app.js changes them later.
|
||||
(function () {
|
||||
var root = document.documentElement, theme = null, display = null;
|
||||
try { theme = localStorage.getItem('pu-theme'); display = localStorage.getItem('pu-display'); } catch (e) {}
|
||||
if (theme !== 'light' && theme !== 'dark') {
|
||||
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
if (theme === 'light') root.classList.add('t-light');
|
||||
root.classList.add(display === 'icon' || display === 'text' ? 'd-' + display : 'd-both');
|
||||
})();
|
||||
</script>
|
||||
<link rel="preload" href="/static/schibsted-grotesk-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/htmx-ext-sse.js"></script>
|
||||
<script src="/static/app.js" defer></script>
|
||||
</head>
|
||||
<body hx-headers='{"X-CSRF-Token": "{{.CSRF}}"}' hx-ext="sse" sse-connect="/events">
|
||||
<nav>
|
||||
<a href="/">Services</a>
|
||||
<a href="/jobs">Jobs</a>
|
||||
<a href="/agents">Agents</a>
|
||||
<form method="post" action="/auth/logout">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
||||
<button type="submit">Log out</button>
|
||||
</form>
|
||||
</nav>
|
||||
<main>{{template "content" .}}</main>
|
||||
<div class="shell">
|
||||
<aside class="side">
|
||||
<div class="brand">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><path d="M3 6h14M3 10h9M3 14h6"></path><path d="M15 16V10m-2.5 2.5L15 10l2.5 2.5"></path></svg>
|
||||
package-updater
|
||||
</div>
|
||||
<div id="snav" hx-get="/fragments/nav?section={{.Nav.Section}}" hx-trigger="sse:services, sse:agents" hx-swap="innerHTML">{{template "nav" .}}</div>
|
||||
{{block "machines" .}}{{end}}
|
||||
<div class="livebox" id="live">
|
||||
<b><span class="pulse" aria-hidden="true"></span>Live</b>
|
||||
<span id="live-age">connecting…</span>
|
||||
</div>
|
||||
<div class="sfoot">
|
||||
<button type="button" class="btn sm quiet" id="theme-toggle">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="7" cy="7" r="5"></circle><path d="M7 2v10" ></path></svg>
|
||||
Theme
|
||||
</button>
|
||||
<form method="post" action="/auth/logout">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
||||
<button type="submit" class="btn sm quiet">Log out</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="work">
|
||||
<main class="main">{{template "content" .}}</main>
|
||||
{{block "drawer" .}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>{{end}}
|
||||
|
||||
{{define "nav"}}<nav class="snav" aria-label="Main">
|
||||
<a href="/"{{if eq .Nav.Section "services"}} class="cur" aria-current="page"{{end}}>Services <span class="n">{{.Nav.Services}}</span></a>
|
||||
<a href="/jobs"{{if eq .Nav.Section "jobs"}} class="cur" aria-current="page"{{end}}>Jobs</a>
|
||||
<a href="/agents"{{if eq .Nav.Section "agents"}} class="cur" aria-current="page"{{end}}>Agents <span class="n">{{.Nav.AgentsOnline}}/{{.Nav.AgentsTotal}}</span></a>
|
||||
</nav>{{end}}
|
||||
|
||||
Reference in New Issue
Block a user