Files
lpra/internal/server/web/templates/dashboard.html
T
redtea 8ba2dd1f1b chore: name the actions column, drop hardcoded tunnel port and 2 KB mentions
The tunnel port default lives only in tunnel.DefaultPort; docs and
comments refer to "the tunnel port". The file manager hint no longer
states an edit limit that was not the real one.
2026-10-13 19:04:22 +03:00

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>