Status page per machine: CPU load, RAM and disk of the client, sampled by the agent only while the page is open and visible (one reading serves every open tab), plus a button that has the agent measure its link to the host: ping, down and up. Online/offline now comes from the control polls the hub holds open, so a dropped agent shows offline at once instead of 45 s later, and the dashboard keeps the status column live. A forward rule can be created "without ufw": the panel opens nothing in ufw for it and so revokes nothing when it is deleted. The checkbox is shown only while the ufw integration is on. Dashboard row actions are icons; a status action is added.
105 lines
5.7 KiB
HTML
105 lines
5.7 KiB
HTML
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Машины — LPRA</title>
|
|
<link rel="stylesheet" href="/static/app.css">
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<div class="brand">LPRA · Удалённый доступ</div>
|
|
<nav>
|
|
<a href="/">Машины</a>
|
|
<a href="/settings">Настройки</a>
|
|
<form method="post" action="/logout" style="display:inline; margin-left:22px">
|
|
<button class="btn ghost small" type="submit">Выйти</button>
|
|
</form>
|
|
</nav>
|
|
</header>
|
|
<div class="wrap">
|
|
<h1>Машины-клиенты</h1>
|
|
<div class="card">
|
|
{{if .Agents}}
|
|
<table>
|
|
<thead><tr><th>Статус</th><th>Имя</th><th>Hostname</th><th>ОС / Арх</th><th>Активность</th><th></th></tr></thead>
|
|
<tbody>
|
|
{{range .Agents}}
|
|
<tr data-agent="{{.ID}}">
|
|
<td class="nowrap"><span class="dot {{if .Online}}on{{else}}off{{end}}"></span><span class="state">{{if .Online}}online{{else}}offline{{end}}</span></td>
|
|
<td>{{.Name}}</td>
|
|
<td class="muted">{{.Hostname}}</td>
|
|
<td class="muted nowrap">{{.OS}}/{{.Arch}}</td>
|
|
<td class="muted nowrap seen">{{.Seen}}</td>
|
|
<td class="actions">
|
|
<div class="row" style="gap:6px">
|
|
<a class="btn small icon" href="/agents/{{.ID}}/terminal" title="Терминал">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
|
|
</a>
|
|
<a class="btn ghost small icon" href="/agents/{{.ID}}/files" title="Файлы">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
</a>
|
|
<a class="btn ghost small icon" href="/agents/{{.ID}}/forwards" title="Порты">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
|
</a>
|
|
<a class="btn ghost small icon" href="/agents/{{.ID}}/status" title="Статус">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
</a>
|
|
<a class="btn ghost small icon" href="/agents/{{.ID}}/edit" title="Изменить">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
|
|
</a>
|
|
<form method="post" action="/agents/{{.ID}}/delete" style="display:inline"
|
|
onsubmit="return confirm('Удалить машину {{.Name}}? Сервис на ней продолжит работать, но перестанет быть виден. Чтобы снова подключиться — переустановите агента.')">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<button class="btn danger small icon" type="submit" title="Удалить">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{else}}
|
|
<p class="muted">Пока нет подключённых машин. Добавьте первую в разделе
|
|
<a href="/settings">Настройки</a> — там одна команда для установки.</p>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// Keep the status column live; nothing is asked while the tab is hidden.
|
|
(function () {
|
|
var rows = {};
|
|
document.querySelectorAll('tr[data-agent]').forEach(function (tr) { rows[tr.dataset.agent] = tr; });
|
|
var timer = null, busy = false;
|
|
function schedule() {
|
|
clearTimeout(timer);
|
|
if (!document.hidden) timer = setTimeout(load, 5000);
|
|
}
|
|
function load() {
|
|
if (busy || document.hidden) return;
|
|
busy = true;
|
|
fetch('/api/panel/agents/presence', {credentials: 'same-origin'})
|
|
.then(function (r) { return r.ok ? r.json() : []; })
|
|
.then(function (list) {
|
|
list.forEach(function (a) {
|
|
var tr = rows[a.id];
|
|
if (!tr) return;
|
|
tr.querySelector('.dot').className = 'dot ' + (a.online ? 'on' : 'off');
|
|
tr.querySelector('.state').textContent = a.online ? 'online' : 'offline';
|
|
tr.querySelector('.seen').textContent = a.seen;
|
|
});
|
|
})
|
|
.catch(function () {})
|
|
.then(function () { busy = false; schedule(); });
|
|
}
|
|
document.addEventListener('visibilitychange', function () {
|
|
if (document.hidden) clearTimeout(timer); else load();
|
|
});
|
|
if (Object.keys(rows).length) schedule();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|