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.
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>
|