net-tools/gui/index.html
Natalie 68c848dc56 feat(@tools/net-tools): add tray icon system
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-10 02:20:23 -07:00

126 lines
6.1 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mesh control</title>
<style>
:root { color-scheme: light dark; }
body { font-family: -apple-system, "Helvetica Neue", sans-serif; margin: 0;
background: Canvas; color: CanvasText; user-select: none; }
#banner { display: flex; align-items: center; gap: 12px; padding: 14px 16px; }
#banner.home { background: #e6f4ea; color: #1e6b34; }
#banner.away { background: #e8f0fe; color: #1a56b0; }
#banner.unknown { background: #fef3e0; color: #8a5a00; }
#banner b { display: block; font-size: 15px; }
#banner small { font-size: 12px; opacity: .85; }
.row { display: flex; align-items: center; gap: 12px; padding: 10px 16px;
border-bottom: 0.5px solid color-mix(in srgb, CanvasText 12%, transparent); }
.row:hover { background: color-mix(in srgb, CanvasText 5%, transparent); }
.dot { width: 10px; height: 10px; border-radius: 50%; background: #999; flex: none; }
.dot.ok { background: #2da44e; } .dot.bad { background: #d1242f; } .dot.idle { background: #bbb; }
.meta { flex: 1; min-width: 0; }
.meta b { font-size: 14px; font-weight: 600; }
.meta b small { font-weight: 400; opacity: .55; font-size: 11px; }
.meta span { display: block; font-size: 12px; opacity: .65; }
.ip { font-family: ui-monospace, monospace; font-size: 12px; opacity: .5; }
#foot { display: flex; justify-content: space-between; align-items: center;
padding: 10px 16px; font-size: 12px; opacity: .7; }
#menu { position: fixed; display: none; min-width: 220px; background: Canvas;
border: 0.5px solid color-mix(in srgb, CanvasText 30%, transparent);
border-radius: 8px; padding: 4px 0; font-size: 13px; z-index: 10;
box-shadow: 0 8px 24px rgba(0,0,0,.18); }
#menu p { margin: 0; padding: 7px 14px; cursor: default; }
#menu p:hover { background: color-mix(in srgb, CanvasText 8%, transparent); }
#menu .hdr { font-size: 11px; opacity: .5; cursor: default; }
#menu .hdr:hover { background: none; }
#out { white-space: pre-wrap; font-family: ui-monospace, monospace; font-size: 11.5px;
padding: 10px 16px; display: none; border-top: 0.5px solid
color-mix(in srgb, CanvasText 12%, transparent); max-height: 180px; overflow-y: auto; }
</style>
</head>
<body>
<div id="banner" class="unknown"><div><b id="loc">Checking where you are…</b><small id="locsub"></small></div></div>
<div id="hosts"></div>
<div id="foot"><span>right-click a device for the power tools</span><span id="agent"></span></div>
<div id="out"></div>
<div id="menu"></div>
<script>
let MENU = document.getElementById('menu');
let fleet = null;
function el(tag, cls, html) { const e = document.createElement(tag); if (cls) e.className = cls; if (html !== undefined) e.innerHTML = html; return e; }
async function refresh() {
fleet = await window.pywebview.api.fleet();
const b = document.getElementById('banner');
if (fleet.location === 'HOME') {
b.className = 'home';
document.getElementById('loc').textContent = "You're home — fast lane is on";
document.getElementById('locsub').textContent = 'devices talk directly (via ' + (fleet.route || '?') + '), not via Iceland';
} else if (fleet.location === 'AWAY') {
b.className = 'away';
document.getElementById('loc').textContent = "You're away — secure tunnel to home";
document.getElementById('locsub').textContent = 'everything still works, a bit slower (via Iceland)';
} else {
b.className = 'unknown';
document.getElementById('loc').textContent = 'Agent not reporting';
document.getElementById('locsub').textContent = 'run: sudo smart-lan-router/install-agent.sh';
}
const age = fleet.agent_ts ? Math.round(Date.now() / 1000 - fleet.agent_ts) : null;
document.getElementById('agent').textContent =
age === null ? 'agent: no status' : (age > 90 ? 'agent: STALE ' + age + 's' : 'agent: ok (' + age + 's)');
const wrap = document.getElementById('hosts');
wrap.textContent = '';
for (const h of fleet.hosts) {
const row = el('div', 'row');
const dot = el('span', 'dot' + (h.phone ? ' idle' : ''));
const meta = el('div', 'meta');
const alias = h.aliases.length ? ' <small>(' + h.aliases[0] + ')</small>' : '';
meta.appendChild(el('b', null, h.name + alias));
const sub = el('span', null, h.friendly + (h.phone ? ' · tunnel client' : ' · checking…'));
meta.appendChild(sub);
row.appendChild(dot); row.appendChild(meta);
row.appendChild(el('span', 'ip', h.ip || ''));
row.oncontextmenu = (ev) => { ev.preventDefault(); openMenu(ev, h); };
wrap.appendChild(row);
if (!h.phone && h.ip) probeRow(h, dot, sub);
}
}
async function probeRow(h, dot, sub) {
const r = await window.pywebview.api.probe(h.ip);
if (r.ok) { dot.className = 'dot ok'; sub.textContent = h.friendly + ' · online · ' + (r.ms < 30 ? 'fast (' + r.ms + ' ms)' : r.ms + ' ms'); }
else { dot.className = 'dot bad'; sub.textContent = h.friendly + ' · not answering'; }
}
function item(label, fn) { const p = el('p', null, label); p.onclick = () => { hideMenu(); fn(); }; return p; }
function openMenu(ev, h) {
MENU.textContent = '';
MENU.appendChild(el('p', 'hdr', h.name + ' — advanced'));
MENU.appendChild(item('Copy address', () => window.pywebview.api.copy(h.ip)));
if (!h.phone) {
MENU.appendChild(item('Open terminal here (ssh)', () => window.pywebview.api.ssh_terminal(h.name)));
MENU.appendChild(item('Diagnose path…', () => runDoctor(h.name)));
if (h.wg) MENU.appendChild(item('Copy tunnel address (.wg)', () => window.pywebview.api.copy(h.wg)));
}
MENU.style.display = 'block';
MENU.style.left = Math.min(ev.clientX, window.innerWidth - 240) + 'px';
MENU.style.top = Math.min(ev.clientY, window.innerHeight - 160) + 'px';
}
function hideMenu() { MENU.style.display = 'none'; }
document.addEventListener('click', hideMenu);
async function runDoctor(name) {
const out = document.getElementById('out');
out.style.display = 'block';
out.textContent = 'diagnosing ' + name + '…';
out.textContent = await window.pywebview.api.doctor(name);
}
window.addEventListener('pywebviewready', () => { refresh(); setInterval(refresh, 30000); });
</script>
</body>
</html>