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
101 lines
5.2 KiB
HTML
101 lines
5.2 KiB
HTML
{{define "content"}}
|
|
<div class="hdr ruled">
|
|
<h1>Agents</h1>
|
|
<span class="sub">{{.Nav.AgentsTotal}} {{if eq .Nav.AgentsTotal 1}}machine{{else}}machines{{end}}{{if .MasterVersion}} · master {{.MasterVersion}}{{end}}</span>
|
|
<div class="end"><a class="btn pri" href="/agents?add=1">Add agent</a></div>
|
|
</div>
|
|
<div id="agents" hx-get="/fragments/agents" hx-trigger="sse:agents" hx-swap="innerHTML">
|
|
{{template "agents" .}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "agents"}}
|
|
{{range .Agents}}
|
|
<div class="agent">
|
|
<div class="id"><b>{{.Name}}</b><span>{{.Hostname}}</span></div>
|
|
<div class="stc"><span class="st {{.StatusClass}}"><i></i>{{.Status}}</span></div>
|
|
<div class="ver{{if .VersionOld}} old{{end}}"><span class="k">Agent</span>{{.AgentVersion}}</div>
|
|
<div class="mgr"><span class="k">Swarm</span>{{.Role}}</div>
|
|
<div class="last"><span class="k">Last seen</span>{{if .Online}}now{{else}}{{with .LastSeenAt}}<time datetime="{{.Format "2006-01-02T15:04:05Z07:00"}}" data-rel>{{.Format "2006-01-02 15:04"}}</time>{{else}}—{{end}}{{end}}</div>
|
|
<div class="act">{{if not .RevokedAt}}
|
|
<details class="rv" data-id="{{.ID}}">
|
|
<summary class="btn sm red">Revoke</summary>
|
|
<div class="pop" role="dialog" aria-label="Confirm revoke">
|
|
<h4>Revoke {{.Name}}?</h4>
|
|
<p>Its token stops working immediately. Services on {{.Name}} keep running, but drop out of package-updater.</p>
|
|
<form method="post" action="/agents/{{.ID}}/revoke" class="acts">
|
|
<input type="hidden" name="csrf_token" value="{{$.CSRF}}">
|
|
<button type="button" class="btn sm quiet" data-close>Cancel</button>
|
|
<button type="submit" class="btn sm redfill">Revoke</button>
|
|
</form>
|
|
</div>
|
|
</details>
|
|
{{end}}</div>
|
|
</div>
|
|
{{else}}
|
|
<p class="nomatch">No agents yet.</p>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{/* The "add agent" panel beside the list. Step 1 asks for a name, step 2 shows the one-time token, step 3 waits for the agent to connect. */}}
|
|
{{define "drawer"}}
|
|
{{if .AddStep}}
|
|
<section class="drawer" aria-label="Add agent">
|
|
<div class="dh">
|
|
<h2>Add agent</h2>
|
|
<a class="btn sm quiet x" href="/agents" aria-label="Close">
|
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M3 3l8 8M11 3l-8 8"></path></svg>
|
|
</a>
|
|
</div>
|
|
{{if eq .AddStep 1}}
|
|
<div class="steps" id="add-steps">{{template "steps" 1}}</div>
|
|
<form method="post" action="/agents" class="dform">
|
|
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
|
<label class="field">Name
|
|
<input name="name" required autofocus autocomplete="off" placeholder="node-2" value="{{.FormName}}">
|
|
<small>Lowercase letters, digits and dashes. Use the machine's name.</small>
|
|
</label>
|
|
{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
|
|
<div class="df">
|
|
<a class="btn quiet" href="/agents">Cancel</a>
|
|
<button type="submit" class="btn pri">Create agent</button>
|
|
</div>
|
|
</form>
|
|
{{else}}
|
|
<div class="steps" id="add-steps">{{template "steps" 2}}</div>
|
|
<div class="namebox"><span>Name</span><code>{{.Created.Name}}</code></div>
|
|
<div class="notice" role="note">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M8 1.8L15 14H1L8 1.8z"></path><path d="M8 6.5v3.5M8 12v.01"></path></svg>
|
|
<span><b>This token is shown only once.</b> Save the snippet as <code>docker-compose.yml</code> on the machine and run <code>docker compose up -d</code>.</span>
|
|
</div>
|
|
<div class="code">
|
|
<div class="code-h"><span>docker-compose.yml</span><button type="button" class="btn sm" data-copy-target="snippet">Copy</button></div>
|
|
<pre id="snippet">{{.Snippet}}</pre>
|
|
</div>
|
|
<div class="secret">
|
|
<span>Token</span><code>{{middle .Token}}</code><button type="button" class="btn sm" data-copy="{{.Token}}">Copy</button>
|
|
<span>Master key</span><code>{{middle .MasterKey}}</code><button type="button" class="btn sm" data-copy="{{.MasterKey}}">Copy</button>
|
|
</div>
|
|
<div class="df" id="connect" hx-get="/fragments/agents/{{.Created.ID}}/connect" hx-trigger="load, sse:agents" hx-swap="innerHTML">
|
|
{{template "connect" .}}
|
|
</div>
|
|
{{end}}
|
|
</section>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{/* step is the one in progress (1 to 3); 4 marks all three as done. */}}
|
|
{{define "steps"}}<span class="{{if gt . 1}}done{{else}}now{{end}}"><b>1</b>Name</span><span class="sep"></span><span class="{{if gt . 2}}done{{else if eq . 2}}now{{end}}"><b>2</b>Deploy</span><span class="sep"></span><span class="{{if gt . 3}}done{{else if eq . 3}}now{{end}}"><b>3</b>Connect</span>{{end}}
|
|
|
|
{{/* Step 3. Until the agent has connected, Cancel revokes the token that was just created. */}}
|
|
{{define "connect"}}{{if .Connected}}<span class="ok">Connected · {{.Created.Hostname}} · {{.Created.AgentVersion}}</span>
|
|
<a class="btn pri" href="/agents">Done</a>
|
|
<div id="add-steps" hx-swap-oob="innerHTML">{{template "steps" 4}}</div>
|
|
{{else}}<span><span class="spin" aria-hidden="true"></span>Waiting for {{.Created.Name}}…</span>
|
|
<form method="post" action="/agents/{{.Created.ID}}/revoke">
|
|
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
|
<button type="submit" class="btn quiet">Cancel</button>
|
|
</form>
|
|
<a class="btn pri" href="/agents">Done</a>
|
|
{{end}}{{end}}
|