2026-08-28 11:22:32 +02:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<!--
|
|
|
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
De statuspagina van Evolu Relay.
|
|
|
|
|
|
|
|
|
|
Zichtbare tekst is Engels: dit is een publieke app store en dit is wat een
|
|
|
|
|
bezoeker ziet. Commentaar is Nederlands.
|
|
|
|
|
|
|
|
|
|
LET OP, en dit is de valstrik van dit bestand: umbreld haalt elke *.template
|
|
|
|
|
bij het starten door envsubst. Er mag dus GEEN dollarteken in staan. Dat sluit
|
|
|
|
|
ook JavaScript-template-literals uit, want die gebruiken accolades achter een
|
|
|
|
|
dollarteken en zouden stilzwijgend leeg worden. Alles hieronder plakt strings
|
|
|
|
|
dus met een plus aan elkaar. tests/test_appstore_vorm.py houdt dit dicht.
|
|
|
|
|
|
|
|
|
|
Het ontwerp volgt HomeGit/Docs/website-design-system.html, net als Electrum
|
|
|
|
|
Gate, zodat de twee apps naast elkaar op hetzelfde dashboard herkenbaar zijn.
|
|
|
|
|
Wat er bewust NIET uit overgenomen is: de verwijzing naar Google Fonts. Een app
|
|
|
|
|
op een Umbrel hoort niet te wachten op een lettertype van buiten.
|
|
|
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
-->
|
|
|
|
|
<html lang="en" data-theme="dark" data-accent="orange">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>Evolu Relay</title>
|
|
|
|
|
<link rel="icon" href="icon.png">
|
|
|
|
|
<style>
|
|
|
|
|
:root {
|
|
|
|
|
--radius-sm: 8px;
|
|
|
|
|
--radius-md: 14px;
|
|
|
|
|
--radius-lg: 20px;
|
|
|
|
|
--radius-full: 999px;
|
|
|
|
|
--font-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
|
|
|
--font-mono: 'SF Mono', 'Fira Code', ui-monospace, Menlo, Consolas, monospace;
|
|
|
|
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-theme="dark"][data-accent="orange"] {
|
|
|
|
|
--bg: #080808;
|
|
|
|
|
--bg-elevated: #181818;
|
|
|
|
|
--bg-raised: #242424;
|
|
|
|
|
--border: rgba(255,255,255,0.18);
|
|
|
|
|
--text-primary: #f2f2f2;
|
|
|
|
|
--text-sec: #a3a3a3;
|
|
|
|
|
--text-ter: #6a6a6a;
|
|
|
|
|
--accent: #FF6B00;
|
|
|
|
|
--accent-soft: rgba(255,107,0,0.16);
|
|
|
|
|
--pos: #FF8124;
|
|
|
|
|
--pos-bg: rgba(255,107,0,0.18);
|
|
|
|
|
--neg: #ff5a5a;
|
|
|
|
|
--neg-bg: rgba(255,69,69,0.18);
|
|
|
|
|
--neutral-bg: rgba(255,255,255,0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-theme="light"][data-accent="orange"] {
|
|
|
|
|
--bg: #ffffff;
|
|
|
|
|
--bg-elevated: #f7f7f7;
|
|
|
|
|
--bg-raised: #f0f0f0;
|
|
|
|
|
--border: rgba(0,0,0,0.12);
|
|
|
|
|
--text-primary: #0a0a0a;
|
|
|
|
|
--text-sec: #888888;
|
|
|
|
|
--text-ter: #bbbbbb;
|
|
|
|
|
--accent: #E05500;
|
|
|
|
|
--accent-soft: rgba(224,85,0,0.08);
|
|
|
|
|
--pos: #D04800;
|
|
|
|
|
--pos-bg: rgba(224,85,0,0.08);
|
|
|
|
|
--neg: #cc2222;
|
|
|
|
|
--neg-bg: rgba(204,34,34,0.08);
|
|
|
|
|
--neutral-bg: rgba(0,0,0,0.04);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 24px 16px 64px;
|
|
|
|
|
background: var(--bg);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
font-family: var(--font-base);
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
-webkit-font-smoothing: antialiased;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.wrap { max-width: 780px; margin: 0 auto; }
|
|
|
|
|
|
|
|
|
|
header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
header img { width: 40px; height: 40px; border-radius: var(--radius-sm); }
|
|
|
|
|
header h1 { font-size: 1.35rem; margin: 0; font-weight: 700; letter-spacing: -0.01em; }
|
|
|
|
|
header .spacer { flex: 1; }
|
|
|
|
|
|
|
|
|
|
.sub { color: var(--text-sec); margin: 0 0 24px; font-size: 0.92rem; }
|
|
|
|
|
|
|
|
|
|
.theme-toggle {
|
|
|
|
|
background: var(--bg-raised);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
color: var(--text-sec);
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
padding: 6px 14px;
|
|
|
|
|
font: inherit;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card {
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
padding: 20px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card h2 {
|
|
|
|
|
font-size: 0.78rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
color: var(--text-ter);
|
|
|
|
|
margin: 0 0 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-row { display: flex; flex-wrap: wrap; gap: 24px; }
|
|
|
|
|
.stat { min-width: 130px; }
|
|
|
|
|
.stat-label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-ter); }
|
|
|
|
|
.stat-val { font-size: 1.15rem; font-weight: 600; margin-top: 2px; }
|
|
|
|
|
.stat-val.mono { font-family: var(--font-mono); font-size: 0.95rem; }
|
|
|
|
|
|
|
|
|
|
.pill {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 3px 10px;
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
.pill.ok { background: var(--pos-bg); color: var(--pos); }
|
|
|
|
|
.pill.bad { background: var(--neg-bg); color: var(--neg); }
|
|
|
|
|
.pill.unknown { background: var(--neutral-bg); color: var(--text-sec); }
|
|
|
|
|
|
|
|
|
|
.url-box {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
background: var(--bg-raised);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hint { color: var(--text-sec); font-size: 0.85rem; margin-top: 10px; }
|
|
|
|
|
|
|
|
|
|
.switch-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
|
|
|
|
|
.switch-row .grow { flex: 1; min-width: 200px; }
|
|
|
|
|
|
|
|
|
|
button.action {
|
|
|
|
|
background: var(--accent-soft);
|
|
|
|
|
border: 1px solid var(--accent);
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
padding: 8px 18px;
|
|
|
|
|
font: inherit;
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: transform 0.15s var(--ease-out);
|
|
|
|
|
}
|
|
|
|
|
button.action:hover:not(:disabled) { transform: translateY(-1px); }
|
|
|
|
|
button.action:disabled { opacity: 0.45; cursor: default; }
|
|
|
|
|
button.action.quiet { background: transparent; border-color: var(--border); color: var(--text-sec); }
|
|
|
|
|
button.action.danger { background: var(--neg-bg); border-color: var(--neg); color: var(--neg); }
|
|
|
|
|
|
|
|
|
|
.owner {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 12px 0;
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
.owner:first-of-type { border-top: none; }
|
|
|
|
|
.owner .id {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 220px;
|
|
|
|
|
}
|
|
|
|
|
.owner .meta { color: var(--text-ter); font-size: 0.75rem; }
|
|
|
|
|
.owner .buttons { display: flex; gap: 8px; }
|
|
|
|
|
|
|
|
|
|
.empty { color: var(--text-ter); font-size: 0.88rem; font-style: italic; }
|
|
|
|
|
|
|
|
|
|
.notice {
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
font-size: 0.88rem;
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
.notice.show { display: block; }
|
|
|
|
|
.notice.warn { background: var(--neg-bg); color: var(--neg); }
|
|
|
|
|
.notice.info { background: var(--accent-soft); color: var(--accent); }
|
|
|
|
|
|
|
|
|
|
footer { color: var(--text-ter); font-size: 0.78rem; text-align: center; margin-top: 32px; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="wrap">
|
|
|
|
|
|
|
|
|
|
<header>
|
|
|
|
|
<img src="icon.png" alt="">
|
|
|
|
|
<h1>Evolu Relay</h1>
|
|
|
|
|
<span class="spacer"></span>
|
|
|
|
|
<button class="theme-toggle" id="theme">Light</button>
|
|
|
|
|
</header>
|
2026-08-28 11:49:15 +02:00
|
|
|
<p class="sub">Encrypted sync and backup for your local-first apps, on hardware you own.</p>
|
2026-08-28 11:22:32 +02:00
|
|
|
|
|
|
|
|
<div class="notice" id="notice"></div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h2>Relay</h2>
|
|
|
|
|
<div class="stat-row">
|
|
|
|
|
<div class="stat">
|
|
|
|
|
<div class="stat-label">Status</div>
|
|
|
|
|
<div class="stat-val"><span class="pill unknown" id="relay-state">checking</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat">
|
|
|
|
|
<div class="stat-label">Stored data</div>
|
|
|
|
|
<div class="stat-val mono" id="db-size">-</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat">
|
|
|
|
|
<div class="stat-label">Last write</div>
|
|
|
|
|
<div class="stat-val mono" id="db-modified">-</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-08-28 11:49:15 +02:00
|
|
|
<h2>Point your app here</h2>
|
2026-08-28 11:22:32 +02:00
|
|
|
<div class="url-box" id="relay-url">-</div>
|
|
|
|
|
<p class="hint">
|
2026-08-28 11:49:15 +02:00
|
|
|
Set this as the sync address in your Evolu app. Some apps only offer the field in their developer
|
|
|
|
|
or advanced settings. The address has to start with http or https, not ws, even though the
|
|
|
|
|
connection itself is a websocket: the client works that out on its own.
|
2026-08-28 11:22:32 +02:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h2>New owners</h2>
|
|
|
|
|
<div class="switch-row">
|
|
|
|
|
<div class="grow">
|
|
|
|
|
<div class="stat-val"><span class="pill unknown" id="learning-state">unknown</span></div>
|
|
|
|
|
<p class="hint" id="learning-hint">
|
|
|
|
|
While this is open, the next device that connects is accepted and remembered. Close it once your
|
|
|
|
|
own devices are paired.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="action" id="learning-toggle" disabled>Loading</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h2>Accepted owners</h2>
|
|
|
|
|
<div id="allowed"><p class="empty">None yet.</p></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h2>Blocked owners</h2>
|
|
|
|
|
<div id="blocked"><p class="empty">None.</p></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h2>Refused attempts</h2>
|
|
|
|
|
<div id="rejected"><p class="empty">None.</p></div>
|
|
|
|
|
<p class="hint">
|
|
|
|
|
Devices that tried to connect while new owners were closed. Only the twenty most recent are kept.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<footer>Evolu Relay on umbrelOS · WhatsNext?</footer>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function () {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
var root = document.documentElement;
|
|
|
|
|
var noticeBox = document.getElementById('notice');
|
|
|
|
|
var busy = false;
|
|
|
|
|
|
|
|
|
|
// ── Thema ──────────────────────────────────────────────────────────────────
|
|
|
|
|
var themeButton = document.getElementById('theme');
|
|
|
|
|
var applyTheme = function (theme) {
|
|
|
|
|
root.setAttribute('data-theme', theme);
|
|
|
|
|
themeButton.textContent = theme === 'dark' ? 'Light' : 'Dark';
|
|
|
|
|
try { localStorage.setItem('evolu-relay-theme', theme); } catch (error) { /* privémodus */ }
|
|
|
|
|
};
|
|
|
|
|
try {
|
|
|
|
|
var saved = localStorage.getItem('evolu-relay-theme');
|
|
|
|
|
if (saved === 'light' || saved === 'dark') applyTheme(saved);
|
|
|
|
|
} catch (error) { /* privémodus */ }
|
|
|
|
|
themeButton.addEventListener('click', function () {
|
|
|
|
|
applyTheme(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Hulpjes ────────────────────────────────────────────────────────────────
|
|
|
|
|
var notice = function (text, kind) {
|
|
|
|
|
noticeBox.textContent = text;
|
|
|
|
|
noticeBox.className = 'notice show ' + (kind || 'info');
|
|
|
|
|
};
|
|
|
|
|
var clearNotice = function () { noticeBox.className = 'notice'; };
|
|
|
|
|
|
|
|
|
|
var formatBytes = function (bytes) {
|
|
|
|
|
if (typeof bytes !== 'number') return '-';
|
|
|
|
|
if (bytes < 1024) return bytes + ' B';
|
|
|
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' kB';
|
|
|
|
|
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var formatTime = function (value) {
|
|
|
|
|
if (typeof value !== 'string') return 'never';
|
|
|
|
|
var when = new Date(value);
|
|
|
|
|
if (isNaN(when.getTime())) return 'never';
|
|
|
|
|
return when.toLocaleString();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var setPill = function (element, text, kind) {
|
|
|
|
|
element.textContent = text;
|
|
|
|
|
element.className = 'pill ' + kind;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var shorten = function (ownerId) {
|
|
|
|
|
if (ownerId.length <= 24) return ownerId;
|
|
|
|
|
return ownerId.slice(0, 12) + '…' + ownerId.slice(-8);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Opdrachten ─────────────────────────────────────────────────────────────
|
|
|
|
|
var send = function (command) {
|
|
|
|
|
if (busy) return;
|
|
|
|
|
busy = true;
|
|
|
|
|
clearNotice();
|
|
|
|
|
fetch('api/command', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(command)
|
|
|
|
|
}).then(function (response) {
|
|
|
|
|
return response.json().then(function (body) { return { ok: response.ok, body: body }; });
|
|
|
|
|
}).then(function (result) {
|
|
|
|
|
if (!result.ok) {
|
|
|
|
|
notice('The relay refused that: ' + (result.body.error || 'unknown reason'), 'warn');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
notice('Sent. The relay applies changes within a few seconds.', 'info');
|
|
|
|
|
}).catch(function () {
|
|
|
|
|
notice('Could not reach the agent.', 'warn');
|
|
|
|
|
}).then(function () {
|
|
|
|
|
busy = false;
|
|
|
|
|
window.setTimeout(refresh, 2500);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var ownerRow = function (entry, buttons) {
|
|
|
|
|
var row = document.createElement('div');
|
|
|
|
|
row.className = 'owner';
|
|
|
|
|
|
|
|
|
|
var id = document.createElement('div');
|
|
|
|
|
id.className = 'id';
|
|
|
|
|
id.textContent = shorten(entry.id);
|
|
|
|
|
id.title = entry.id;
|
|
|
|
|
row.appendChild(id);
|
|
|
|
|
|
|
|
|
|
var meta = document.createElement('div');
|
|
|
|
|
meta.className = 'meta';
|
|
|
|
|
var parts = [];
|
|
|
|
|
if (entry.firstSeen) parts.push('first seen ' + formatTime(entry.firstSeen));
|
|
|
|
|
if (entry.lastSeen) parts.push('last seen ' + formatTime(entry.lastSeen));
|
|
|
|
|
if (entry.attempts) parts.push(entry.attempts + ' attempts');
|
|
|
|
|
meta.textContent = parts.join(' · ');
|
|
|
|
|
row.appendChild(meta);
|
|
|
|
|
|
|
|
|
|
var holder = document.createElement('div');
|
|
|
|
|
holder.className = 'buttons';
|
|
|
|
|
buttons.forEach(function (spec) {
|
|
|
|
|
var button = document.createElement('button');
|
|
|
|
|
button.className = 'action ' + (spec.style || 'quiet');
|
|
|
|
|
button.textContent = spec.label;
|
|
|
|
|
button.addEventListener('click', function () { send(spec.command(entry.id)); });
|
|
|
|
|
holder.appendChild(button);
|
|
|
|
|
});
|
|
|
|
|
row.appendChild(holder);
|
|
|
|
|
|
|
|
|
|
return row;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var fillList = function (element, entries, emptyText, buttons) {
|
|
|
|
|
element.textContent = '';
|
|
|
|
|
if (!entries.length) {
|
|
|
|
|
var empty = document.createElement('p');
|
|
|
|
|
empty.className = 'empty';
|
|
|
|
|
empty.textContent = emptyText;
|
|
|
|
|
element.appendChild(empty);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
entries.forEach(function (entry) { element.appendChild(ownerRow(entry, buttons)); });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── De ronde ───────────────────────────────────────────────────────────────
|
|
|
|
|
var refresh = function () {
|
|
|
|
|
fetch('api/status', { cache: 'no-store' }).then(function (response) {
|
|
|
|
|
return response.json();
|
|
|
|
|
}).then(function (status) {
|
|
|
|
|
var relayPill = document.getElementById('relay-state');
|
|
|
|
|
if (status.relay.reachable === true) setPill(relayPill, 'running', 'ok');
|
|
|
|
|
else if (status.relay.reachable === false) setPill(relayPill, 'not responding', 'bad');
|
|
|
|
|
else setPill(relayPill, 'unknown', 'unknown');
|
|
|
|
|
|
|
|
|
|
document.getElementById('db-size').textContent = formatBytes(status.database.bytes);
|
|
|
|
|
document.getElementById('db-modified').textContent = formatTime(status.database.modified);
|
|
|
|
|
|
|
|
|
|
document.getElementById('relay-url').textContent =
|
|
|
|
|
'http://' + window.location.hostname + ':' + status.relay.publicPort;
|
|
|
|
|
|
|
|
|
|
var learningPill = document.getElementById('learning-state');
|
|
|
|
|
var toggle = document.getElementById('learning-toggle');
|
|
|
|
|
if (status.owners.learning === true) {
|
|
|
|
|
setPill(learningPill, 'open', 'ok');
|
|
|
|
|
toggle.textContent = 'Close';
|
|
|
|
|
toggle.disabled = false;
|
|
|
|
|
} else if (status.owners.learning === false) {
|
|
|
|
|
setPill(learningPill, 'closed', 'unknown');
|
|
|
|
|
toggle.textContent = 'Open';
|
|
|
|
|
toggle.disabled = false;
|
|
|
|
|
} else {
|
|
|
|
|
setPill(learningPill, 'unknown', 'unknown');
|
|
|
|
|
toggle.textContent = 'Loading';
|
|
|
|
|
toggle.disabled = true;
|
|
|
|
|
}
|
|
|
|
|
toggle.onclick = function () {
|
|
|
|
|
send({ action: 'set-learning', value: status.owners.learning !== true });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fillList(document.getElementById('allowed'), status.owners.allowed, 'None yet.', [
|
|
|
|
|
{ label: 'Block', style: 'danger', command: function (id) { return { action: 'block', ownerId: id }; } },
|
|
|
|
|
{ label: 'Forget', command: function (id) { return { action: 'forget', ownerId: id }; } }
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
fillList(document.getElementById('blocked'), status.owners.blocked, 'None.', [
|
|
|
|
|
{ label: 'Allow', command: function (id) { return { action: 'allow', ownerId: id }; } },
|
|
|
|
|
{ label: 'Forget', command: function (id) { return { action: 'forget', ownerId: id }; } }
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
fillList(document.getElementById('rejected'), status.owners.rejected, 'None.', [
|
|
|
|
|
{ label: 'Allow', command: function (id) { return { action: 'allow', ownerId: id }; } }
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (status.owners.problem === 'onleesbaar') {
|
|
|
|
|
notice('The owner list could not be read and was moved aside. New owners are refused until you '
|
|
|
|
|
+ 'open them again above.', 'warn');
|
|
|
|
|
} else if (status.pendingCommand) {
|
|
|
|
|
notice('A command is waiting for the relay to pick it up.', 'info');
|
|
|
|
|
} else if (noticeBox.className.indexOf('warn') === -1) {
|
|
|
|
|
clearNotice();
|
|
|
|
|
}
|
|
|
|
|
}).catch(function () {
|
|
|
|
|
setPill(document.getElementById('relay-state'), 'agent unreachable', 'bad');
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
refresh();
|
|
|
|
|
window.setInterval(refresh, 5000);
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|