feat: one-page panel - shell with Машины, Терминал and Настройки, pages as fragments, forms sent in place

This commit is contained in:
redtea committed 2026-10-15 20:05:20 +03:00
1 parent 7b9daa524e
commit 113c63fd1b
17 files changed
+678 -432

No files matched your search

+2 -55
View File
@@ -1,22 +1,4 @@
<!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>
<script src="{{asset "scroll.js"}}" defer></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>
{{define "dashboard-body"}}<div class="page" data-module="dashboard">
<div class="wrap">
<h1>Машины-клиенты</h1>
<div class="card">
@@ -68,39 +50,4 @@
{{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>
</div>{{end}}