feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
20
internal/webapp/templates/app_detail.html
Обычный файл
20
internal/webapp/templates/app_detail.html
Обычный файл
@@ -0,0 +1,20 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>{{.App.Name}}</h1>
|
||||
<dl>
|
||||
<dt>Source</dt><dd>{{.App.Source}}</dd>
|
||||
<dt>PID</dt><dd>{{.App.PID}}</dd>
|
||||
<dt>Open ports</dt>
|
||||
<dd>
|
||||
{{if .App.Ports}}
|
||||
<ul>
|
||||
{{range .App.Ports}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
{{else}}<span class="muted">none</span>{{end}}
|
||||
</dd>
|
||||
<dt>Cmdline</dt><dd><code>{{.App.Cmdline}}</code></dd>
|
||||
<dt>Cwd</dt><dd><code>{{.App.CWD}}</code></dd>
|
||||
<dt>Last seen</dt><dd>{{fmtTime .App.LastSeen}}</dd>
|
||||
<dt>Last check</dt><dd>— <span class="muted">(Phase 1: no per-app probes)</span></dd>
|
||||
</dl>
|
||||
<p><a href="/apps">← All apps</a></p>
|
||||
</section>{{end}}
|
||||
32
internal/webapp/templates/apps.html
Обычный файл
32
internal/webapp/templates/apps.html
Обычный файл
@@ -0,0 +1,32 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Discovered apps</h1>
|
||||
<p class="muted">Refreshed every 60s from <code>/proc</code>. Docker Compose / nginx / systemd discoveries land with the deploymentd integration (see docs/plans/inventory-management.md §7.2); once the host runs <code>deploymentd</code> and posts to RSMon, the matching Site + Deployment rows appear here side-by-side with the per-process list.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Source</th>
|
||||
<th>PID</th>
|
||||
<th>Ports</th>
|
||||
<th>Uptime</th>
|
||||
<th>Last check</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Apps}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{.Source}}</td>
|
||||
<td>{{.PID}}</td>
|
||||
<td>{{range .Ports}}{{.}}<br>{{end}}</td>
|
||||
<td>{{fmtTime .LastSeen}}</td>
|
||||
<td>—</td>
|
||||
<td><a href="/apps/{{.PID}}">details</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="7" class="muted">No apps discovered yet — first refresh runs within 60s of start.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>{{end}}
|
||||
21
internal/webapp/templates/change_password.html
Обычный файл
21
internal/webapp/templates/change_password.html
Обычный файл
@@ -0,0 +1,21 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Change password</h1>
|
||||
<p class="muted">The operator must change the first-run password before they can use the worker web app.</p>
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<form action="/web/change-password" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<label>
|
||||
Current password
|
||||
<input type="password" name="current_password" required>
|
||||
</label>
|
||||
<label>
|
||||
New password (min {{.MinStrength}} chars)
|
||||
<input type="password" name="new_password" required minlength="{{.MinStrength}}">
|
||||
</label>
|
||||
<label>
|
||||
Confirm new password
|
||||
<input type="password" name="new_password_confirm" required minlength="{{.MinStrength}}">
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</section>{{end}}
|
||||
34
internal/webapp/templates/checks.html
Обычный файл
34
internal/webapp/templates/checks.html
Обычный файл
@@ -0,0 +1,34 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Recent checks</h1>
|
||||
<p class="muted">Last 50 results produced by this worker.</p>
|
||||
<p><button type="button" disabled title="{{.RunNowTooltip}}">Run now</button>
|
||||
<span class="muted">{{.RunNowTooltip}}</span></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Monitor</th>
|
||||
<th>Check</th>
|
||||
<th>Kind</th>
|
||||
<th>Host</th>
|
||||
<th>State</th>
|
||||
<th>Duration (ms)</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Rows}}
|
||||
<tr>
|
||||
<td>{{.MonitorID}}</td>
|
||||
<td>{{.CheckID}}</td>
|
||||
<td>{{.Kind}}</td>
|
||||
<td>{{.Host}}</td>
|
||||
<td>{{.State}}</td>
|
||||
<td>{{.DurationMs}}</td>
|
||||
<td>{{.Error}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="7" class="muted">No checks yet. Once the main app assigns jobs to this worker, results will appear here.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>{{end}}
|
||||
39
internal/webapp/templates/layout.html
Обычный файл
39
internal/webapp/templates/layout.html
Обычный файл
@@ -0,0 +1,39 @@
|
||||
{{define "layout"}}<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>{{.Title}}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="brand">RSMon worker</div>
|
||||
{{if not .IsLogin}}
|
||||
<nav>
|
||||
<a href="/overview">Overview</a>
|
||||
<a href="/apps">Apps</a>
|
||||
<a href="/checks">Checks</a>
|
||||
<a href="/notifications">Notifications</a>
|
||||
<a href="/logs">Logs</a>
|
||||
<a href="/status">Status</a>
|
||||
<a href="/settings">Settings</a>
|
||||
<a href="/updates">Updates</a>
|
||||
</nav>
|
||||
<div class="who">
|
||||
<form action="/web/logout" method="post" class="logout-form">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="link">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</header>
|
||||
<main>
|
||||
{{template "body" .}}
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<span>RSMon worker v{{.Version}} · built {{.BuildDate}}</span>
|
||||
</footer>
|
||||
</body>
|
||||
</html>{{end}}
|
||||
33
internal/webapp/templates/login.html
Обычный файл
33
internal/webapp/templates/login.html
Обычный файл
@@ -0,0 +1,33 @@
|
||||
{{define "body"}}<section class="card login">
|
||||
<h1>RSMon worker login</h1>
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<form action="/web/login" method="post" autocomplete="off">
|
||||
<input type="hidden" name="next" value="{{.NextURL}}">
|
||||
{{if .BasicAuth}}
|
||||
<label>
|
||||
Username
|
||||
<input type="text" name="login" autocomplete="username" autofocus required value="">
|
||||
</label>
|
||||
{{end}}
|
||||
<label>
|
||||
Password
|
||||
<input type="password" name="password" autocomplete="current-password" {{if not .BasicAuth}}autofocus{{end}} required minlength="1">
|
||||
</label>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
{{if .BasicAuth}}
|
||||
<p class="muted">
|
||||
This worker is configured with HTTP basic auth
|
||||
(<code>WORKER_LOGIN</code>/<code>WORKER_PASSWORD</code>). Use the
|
||||
credentials from your environment. You can also pass them as
|
||||
<code>Authorization: Basic</code> on
|
||||
<code>/web/api/*</code> for scripted access.
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="muted">
|
||||
On first start the worker printed a one-time password to its log.
|
||||
Read it via <code>journalctl -u rsmon-worker</code> or your
|
||||
container runtime.
|
||||
</p>
|
||||
{{end}}
|
||||
</section>{{end}}
|
||||
17
internal/webapp/templates/logs.html
Обычный файл
17
internal/webapp/templates/logs.html
Обычный файл
@@ -0,0 +1,17 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Worker logs</h1>
|
||||
<p class="muted">In-memory ring buffer (capacity {{.BufferCap}}, holding {{.BufferSize}} lines). Phase 1 shows worker-binary logs only; stack and host logs land in later phases.</p>
|
||||
<form method="get" action="/logs" class="tail-form">
|
||||
<label>Show last
|
||||
<select name="tail" onchange="this.form.submit()">
|
||||
<option value="200" {{if eq .Tail 200}}selected{{end}}>200</option>
|
||||
<option value="500" {{if eq .Tail 500}}selected{{end}}>500</option>
|
||||
<option value="1000" {{if eq .Tail 1000}}selected{{end}}>1000</option>
|
||||
<option value="5000" {{if eq .Tail 5000}}selected{{end}}>5000</option>
|
||||
</select>
|
||||
</label>
|
||||
<noscript><button type="submit">Apply</button></noscript>
|
||||
</form>
|
||||
<pre class="log">{{range .Lines}}{{.}}
|
||||
{{end}}</pre>
|
||||
</section>{{end}}
|
||||
32
internal/webapp/templates/notifications.html
Обычный файл
32
internal/webapp/templates/notifications.html
Обычный файл
@@ -0,0 +1,32 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Recent notifications</h1>
|
||||
<p class="muted">Last 50 notifications emitted by this worker (Phase 1: selfcheck alerts only).</p>
|
||||
<p><button type="button" disabled title="{{.ResendTooltip}}">Resend selected</button>
|
||||
<span class="muted">{{.ResendTooltip}}</span></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Channel</th>
|
||||
<th>Subject</th>
|
||||
<th>Body</th>
|
||||
<th>Status</th>
|
||||
<th>Error</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Rows}}
|
||||
<tr>
|
||||
<td>{{.Channel}} ({{.Kind}})</td>
|
||||
<td>{{.Subject}}</td>
|
||||
<td><code>{{.Body}}</code></td>
|
||||
<td>{{if .OK}}<span class="ok">delivered</span>{{else}}<span class="error">failed</span>{{end}}</td>
|
||||
<td>{{.Error}}</td>
|
||||
<td>{{fmtTime .At}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="6" class="muted">No notifications yet.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>{{end}}
|
||||
56
internal/webapp/templates/overview.html
Обычный файл
56
internal/webapp/templates/overview.html
Обычный файл
@@ -0,0 +1,56 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Overview</h1>
|
||||
<div class="cards">
|
||||
<div class="metric">
|
||||
<div class="label">Worker ID</div>
|
||||
<div class="value">{{.WorkerID}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Region</div>
|
||||
<div class="value">{{.RegionCode}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">State</div>
|
||||
<div class="value">{{.WorkerState}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Last heartbeat ack</div>
|
||||
<div class="value">{{fmtTime .LastAckAt}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Discovered apps</div>
|
||||
<div class="value">{{.DiscoveredCount}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Checks (last 1000)</div>
|
||||
<div class="value">{{.ResultCount}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Notifications (last 1000)</div>
|
||||
<div class="value">{{.NotifCount}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">CPU</div>
|
||||
<div class="value">{{fmtPercent .Snapshot.CPU.TotalPct}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Memory used</div>
|
||||
<div class="value">{{fmtPercent .Snapshot.Memory.UsedPct}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Uptime</div>
|
||||
<div class="value">{{fmtDuration .Snapshot.Uptime}}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Load (1/5/15)</div>
|
||||
<div class="value">{{printf "%.2f / %.2f / %.2f" .Snapshot.Load.One .Snapshot.Load.Five .Snapshot.Load.Fifteen}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Recent log lines</h2>
|
||||
<pre class="log">{{range .LogTail}}{{.}}
|
||||
{{end}}</pre>
|
||||
<p><a href="/logs">View all logs</a></p>
|
||||
</section>{{end}}
|
||||
31
internal/webapp/templates/settings.html
Обычный файл
31
internal/webapp/templates/settings.html
Обычный файл
@@ -0,0 +1,31 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Settings</h1>
|
||||
<p class="muted">Phase 1 exposes only the worker fields; web-app settings (theme, session timeout, basic-auth password rotate) land in later phases.</p>
|
||||
|
||||
<h2>Worker</h2>
|
||||
<dl>
|
||||
<dt>Worker ID</dt><dd>{{.WorkerID}}</dd>
|
||||
<dt>Region</dt><dd>{{.RegionCode}}</dd>
|
||||
<dt>Version</dt><dd>{{.WorkerVersion}}</dd>
|
||||
<dt>Capabilities</dt><dd>
|
||||
{{if .Capabilities}}
|
||||
<ul>{{range .Capabilities}}<li><code>{{.}}</code></li>{{end}}</ul>
|
||||
{{else}}<span class="muted">—</span>{{end}}
|
||||
</dd>
|
||||
<dt>Last heartbeat ack</dt><dd>{{fmtTime .LastAckAt}}</dd>
|
||||
</dl>
|
||||
|
||||
<h2>Token</h2>
|
||||
<p>
|
||||
Current token: <code>{{.TokenMasked}}</code><br>
|
||||
Last rotated: {{fmtTime .TokenRotatedAt}}
|
||||
</p>
|
||||
<form action="/settings/rotate-token" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="danger"
|
||||
onclick="return confirm('Rotate the worker token? The old token will be invalidated immediately and the websocket will reconnect with the new one.')">
|
||||
Rotate token
|
||||
</button>
|
||||
<span class="muted">Issues a fresh token via the main app API and updates the in-memory runner config. On failure the old token is kept.</span>
|
||||
</form>
|
||||
</section>{{end}}
|
||||
82
internal/webapp/templates/status.html
Обычный файл
82
internal/webapp/templates/status.html
Обычный файл
@@ -0,0 +1,82 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Server status</h1>
|
||||
<p class="muted">Sampled every 5s from <code>/proc</code> and statfs. CLI tools (lsblk, smartctl, sensors) are not bundled with the worker; install them on the host separately if you want richer hardware data. SMART self-tests are out of scope.</p>
|
||||
|
||||
<h2>CPU</h2>
|
||||
<div class="cards">
|
||||
<div class="metric"><div class="label">Total busy</div><div class="value">{{fmtPercent .Snapshot.CPU.TotalPct}}</div></div>
|
||||
<div class="metric"><div class="label">User</div><div class="value">{{fmtPercent .Snapshot.CPU.UserPct}}</div></div>
|
||||
<div class="metric"><div class="label">System</div><div class="value">{{fmtPercent .Snapshot.CPU.SystemPct}}</div></div>
|
||||
<div class="metric"><div class="label">Idle</div><div class="value">{{fmtPercent .Snapshot.CPU.IdlePct}}</div></div>
|
||||
<div class="metric"><div class="label">I/O wait</div><div class="value">{{fmtPercent .Snapshot.CPU.IOWaitPct}}</div></div>
|
||||
</div>
|
||||
|
||||
<h2>Load</h2>
|
||||
<div class="cards">
|
||||
<div class="metric"><div class="label">1 min</div><div class="value">{{printf "%.2f" .Snapshot.Load.One}}</div></div>
|
||||
<div class="metric"><div class="label">5 min</div><div class="value">{{printf "%.2f" .Snapshot.Load.Five}}</div></div>
|
||||
<div class="metric"><div class="label">15 min</div><div class="value">{{printf "%.2f" .Snapshot.Load.Fifteen}}</div></div>
|
||||
</div>
|
||||
|
||||
<h2>Memory</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><th>Total</th><td>{{fmtBytes .Snapshot.Memory.Total}}</td></tr>
|
||||
<tr><th>Available</th><td>{{fmtBytes .Snapshot.Memory.Available}} ({{fmtPercent .Snapshot.Memory.AvailablePct}})</td></tr>
|
||||
<tr><th>Free</th><td>{{fmtBytes .Snapshot.Memory.Free}}</td></tr>
|
||||
<tr><th>Buffers</th><td>{{fmtBytes .Snapshot.Memory.Buffers}}</td></tr>
|
||||
<tr><th>Cached</th><td>{{fmtBytes .Snapshot.Memory.Cached}}</td></tr>
|
||||
<tr><th>Swap total</th><td>{{fmtBytes .Snapshot.Memory.SwapTotal}}</td></tr>
|
||||
<tr><th>Swap free</th><td>{{fmtBytes .Snapshot.Memory.SwapFree}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Uptime</h2>
|
||||
<div class="cards">
|
||||
<div class="metric"><div class="label">Uptime</div><div class="value">{{fmtDuration .Snapshot.Uptime}}</div></div>
|
||||
<div class="metric"><div class="label">Boot at</div><div class="value">{{fmtTime .Snapshot.BootAt}}</div></div>
|
||||
<div class="metric"><div class="label">Last sample</div><div class="value">{{fmtTime .SnapshotAt}}</div></div>
|
||||
</div>
|
||||
|
||||
<h2>Network</h2>
|
||||
<table>
|
||||
<thead><tr><th>Iface</th><th>RX bytes</th><th>TX bytes</th><th>RX pkts</th><th>TX pkts</th><th>RX errs</th><th>TX errs</th><th>RX drops</th><th>TX drops</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Snapshot.Networks}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{fmtBytes .RxBytes}}</td>
|
||||
<td>{{fmtBytes .TxBytes}}</td>
|
||||
<td>{{.RxPkt}}</td>
|
||||
<td>{{.TxPkt}}</td>
|
||||
<td>{{.RxErr}}</td>
|
||||
<td>{{.TxErr}}</td>
|
||||
<td>{{.RxDrop}}</td>
|
||||
<td>{{.TxDrop}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="9" class="muted">No network interfaces detected.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Mount points</h2>
|
||||
<table>
|
||||
<thead><tr><th>Mount</th><th>Device</th><th>FS</th><th>Total</th><th>Free</th><th>Used</th><th>Use%</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Snapshot.Disks}}
|
||||
<tr>
|
||||
<td>{{.Mount}}</td>
|
||||
<td>{{.Device}}</td>
|
||||
<td>{{.FSType}}</td>
|
||||
<td>{{fmtBytes .Total}}</td>
|
||||
<td>{{fmtBytes .Free}}</td>
|
||||
<td>{{fmtBytes .Used}}</td>
|
||||
<td>{{fmtPercent .UsedPct}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="7" class="muted">No mount points parsed from /proc/mounts.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>{{end}}
|
||||
17
internal/webapp/templates/updates.html
Обычный файл
17
internal/webapp/templates/updates.html
Обычный файл
@@ -0,0 +1,17 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Updates</h1>
|
||||
<p class="muted">Current binary version compared against the latest release. When <code>WORKER_RELEASE_URL</code> is unset the "latest known" cell shows the placeholder <code>v1 (dev)</code>. Pull and restart stays disabled until Docker management lands (requires sudo / docker socket access).</p>
|
||||
<dl>
|
||||
<dt>Current version</dt><dd><code>{{.CurrentVersion}}</code></dd>
|
||||
<dt>Latest known</dt><dd><code>{{.LatestKnown}}</code></dd>
|
||||
<dt>In sync?</dt><dd>
|
||||
{{if eq .CurrentVersion .LatestKnown}}
|
||||
<span class="ok">yes</span>
|
||||
{{else}}
|
||||
<span class="warn">update available</span>
|
||||
{{end}}
|
||||
</dd>
|
||||
</dl>
|
||||
<button type="button" disabled title="{{.PullTooltip}}">Pull and restart</button>
|
||||
<p class="muted">{{.PullTooltip}}</p>
|
||||
</section>{{end}}
|
||||
Ссылка в новой задаче
Block a user