135 lines
5.3 KiB
HTML
135 lines
5.3 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>mc-router</title>
|
|
<style>
|
|
html, body { margin: 0; padding: 0; height: 100%; background: #0e0e0e; color: #eee;
|
|
font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 13px; line-height: 1.4; }
|
|
body { display: flex; flex-direction: column; }
|
|
header { padding: 8px 12px; background: #1a1a1a; border-bottom: 1px solid #333;
|
|
display: flex; justify-content: space-between; align-items: center; flex: 0 0 auto; }
|
|
header h1 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
header h1 .meta { color: #888; font-weight: normal; margin-left: 10px; }
|
|
#status { font-size: 12px; color: #6f6; }
|
|
#status.disconnected { color: #f66; }
|
|
#status.connecting { color: #fc6; }
|
|
|
|
main { flex: 1; display: grid; grid-template-rows: 1fr 1fr; min-height: 0; }
|
|
section { padding: 12px 16px; overflow: auto; min-height: 0; }
|
|
section + section { border-top: 1px solid #333; }
|
|
|
|
h2 { margin: 0 0 8px; font-size: 12px; font-weight: 600; color: #888;
|
|
text-transform: uppercase; letter-spacing: 0.6px;
|
|
display: flex; justify-content: space-between; align-items: center; }
|
|
h2 .count { color: #888; font-weight: normal; margin-left: 6px; }
|
|
h2 button { background: transparent; color: #888; border: 1px solid #333;
|
|
padding: 2px 8px; cursor: pointer; font: inherit; font-size: 11px; border-radius: 3px; }
|
|
h2 button:hover { color: #eee; border-color: #6cf; }
|
|
|
|
table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
|
th, td { text-align: left; padding: 4px 10px; border-bottom: 1px solid #1f1f1f; }
|
|
th { color: #888; font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
td.num { text-align: right; }
|
|
.empty { color: #888; padding: 16px 0; }
|
|
|
|
pre.logbox { margin: 0; white-space: pre-wrap; word-break: break-word; }
|
|
.log-line { padding: 0; }
|
|
.log-line .ts { color: #888; margin-right: 8px; }
|
|
.log-line .lvl { margin-right: 6px; font-weight: 600; }
|
|
.log-line.lvl-debug .lvl { color: #888; }
|
|
.log-line.lvl-info .lvl { color: #6cf; }
|
|
.log-line.lvl-warning .lvl, .log-line.lvl-warn .lvl { color: #fc6; }
|
|
.log-line.lvl-error .lvl { color: #f66; }
|
|
.log-line .attrs { color: #888; margin-left: 8px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>mc-router <span class="meta" id="meta">— connecting…</span></h1>
|
|
<span id="status" class="connecting">log stream: connecting…</span>
|
|
</header>
|
|
<main>
|
|
<section>
|
|
<h2><span>Routes <span class="count" id="route-count"></span></span></h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Server address (host)</th>
|
|
<th>Backend</th>
|
|
<th class="num">Active</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="route-rows"></tbody>
|
|
</table>
|
|
<div class="empty" id="route-empty">no routes registered</div>
|
|
</section>
|
|
<section>
|
|
<h2><span>Logs</span><button onclick="document.getElementById('logbox').innerHTML=''">clear</button></h2>
|
|
<pre class="logbox" id="logbox"></pre>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
async function refreshRoutes() {
|
|
try {
|
|
const r = await fetch('./api/routes');
|
|
const j = await r.json();
|
|
const rows = document.getElementById('route-rows');
|
|
const empty = document.getElementById('route-empty');
|
|
const count = document.getElementById('route-count');
|
|
rows.innerHTML = '';
|
|
if (!j.routes || j.routes.length === 0) {
|
|
empty.style.display = '';
|
|
count.textContent = '';
|
|
} else {
|
|
empty.style.display = 'none';
|
|
count.textContent = '(' + j.routes.length + ')';
|
|
for (const r of j.routes) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td>' + r.server_address + '</td>' +
|
|
'<td>' + r.backend + '</td>' +
|
|
'<td class="num">' + r.active_connections + '</td>';
|
|
rows.appendChild(tr);
|
|
}
|
|
}
|
|
document.getElementById('meta').textContent =
|
|
'— ' + j.routes.length + ' routes · ' + (j.total_connections || 0) + ' active conn';
|
|
} catch (e) {
|
|
document.getElementById('meta').textContent = '— api error';
|
|
}
|
|
}
|
|
setInterval(refreshRoutes, 2000);
|
|
refreshRoutes();
|
|
|
|
function startLogStream() {
|
|
const status = document.getElementById('status');
|
|
const box = document.getElementById('logbox');
|
|
const es = new EventSource('./api/logs');
|
|
es.onopen = () => { status.textContent = 'log stream: live'; status.className = ''; };
|
|
es.onerror = () => { status.textContent = 'log stream: reconnecting…'; status.className = 'disconnected'; };
|
|
es.onmessage = ev => {
|
|
let e;
|
|
try { e = JSON.parse(ev.data); } catch { return; }
|
|
const ts = e.time ? e.time.split('T')[1].split('.')[0] : '';
|
|
const div = document.createElement('div');
|
|
const lvl = (e.level || 'info').toLowerCase();
|
|
div.className = 'log-line lvl-' + lvl;
|
|
div.innerHTML =
|
|
'<span class="ts">' + ts + '</span>' +
|
|
'<span class="lvl">' + (e.level || 'info') + '</span>' +
|
|
(e.msg || '') +
|
|
(e.attrs ? '<span class="attrs">' + e.attrs + '</span>' : '');
|
|
box.appendChild(div);
|
|
const parent = box.parentElement;
|
|
if (parent.scrollHeight - parent.scrollTop - parent.clientHeight < 60) {
|
|
parent.scrollTop = parent.scrollHeight;
|
|
}
|
|
while (box.children.length > 1000) box.removeChild(box.firstChild);
|
|
};
|
|
}
|
|
startLogStream();
|
|
</script>
|
|
</body>
|
|
</html>
|