106 lines
6.5 KiB
HTML
106 lines
6.5 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="{{asset "app.css"}}">
|
|
<script src="{{asset "pin.js"}}"></script>
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<div class="brand"><a class="home" href="/" title="К списку машин">LPRA</a> · Удалённый доступ</div>
|
|
<nav>
|
|
<a href="/">Машины</a>
|
|
{{if .IsAdmin}}<a href="/settings">Настройки</a>{{end}}
|
|
<button type="button" class="btn ghost icon acct-btn" id="acct" title="Аккаунт" aria-label="Аккаунт"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></button>
|
|
</nav>
|
|
</header>
|
|
<div class="wrap">
|
|
<h1>Машины-клиенты</h1>
|
|
<div class="card">
|
|
{{if .Agents}}
|
|
<table class="stack">
|
|
<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" data-l="Статус"><span class="dot {{if .Online}}on{{else}}off{{end}}"></span><span class="state">{{if .Online}}online{{else}}offline{{end}}</span></td>
|
|
<td class="lead">{{.Name}}</td>
|
|
<td class="muted" data-l="Hostname">{{.Hostname}}</td>
|
|
<td class="muted nowrap" data-l="ОС / Арх">{{.OS}}/{{.Arch}}</td>
|
|
<td class="muted nowrap seen" data-l="Активность">{{.Seen}}</td>
|
|
<td class="actions">
|
|
<div class="row" style="gap:6px">
|
|
<a class="btn small icon" href="/agents/{{.ID}}/terminal" data-tip="Терминал" aria-label="Терминал">
|
|
<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" data-tip="Файлы" aria-label="Файлы">
|
|
<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" data-tip="Порты" aria-label="Порты">
|
|
<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" data-tip="Статус" aria-label="Статус">
|
|
<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" data-tip="Изменить" aria-label="Изменить">
|
|
<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>
|
|
{{if $.IsAdmin}}<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" data-tip="Удалить" aria-label="Удалить">
|
|
<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>{{end}}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{else}}
|
|
{{if .IsAdmin}}<p class="muted">Пока нет подключённых машин. Добавьте первую в разделе
|
|
<a href="/settings">Настройки</a> - там одна команда для установки.</p>
|
|
{{else}}<p class="muted">Вам пока не выдали ни одной машины - попросите администратора.</p>{{end}}
|
|
{{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>
|
|
<script src="{{asset "account.js"}}"></script>
|
|
</body>
|
|
</html>
|