feat(diagnostic): Liste, wer gerade mit dem RVS-Raum verbunden ist

Einstellungen, zwischen Runtime-Konfiguration und App-Onboarding: alle
Verbindungen im Raum des konfigurierten Tokens — Art, Name, Details,
Adresse, verbunden seit, zuletzt aktiv.

RVS merkt sich pro Verbindung, wer sie ist: aus Nachrichten, die ohnehin
fliessen (sat_hello, host_hello, worker_hello, update_check der App,
llm_cluster_*), oder aus dem neuen client_hello (Bridge, Diagnostic, App
mit Geraetename). Adresse aus X-Forwarded-For hinter einem Reverse-Proxy.
room_clients_query liefert NUR den eigenen Raum; room_clients von Clients
wird nicht weitergereicht. Typen in der Allow-List ergaenzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 12:34:14 +01:00
co-authored by Claude Opus 5.5
parent 2267f0a726
commit 7b4a1f4429
5 changed files with 188 additions and 1 deletions
+64
View File
@@ -1388,6 +1388,22 @@
</div>
</div>
<!-- Wer ist im Raum? -->
<div class="settings-section">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h2 style="margin:0;">Verbunden mit diesem Raum</h2>
<button class="btn secondary" onclick="requestRoomClients()" style="padding:4px 12px;">Aktualisieren</button>
</div>
<div style="font-size:11px;color:#8888AA;margin:6px 0 8px;">
Alle Verbindungen, die gerade mit dem RVS-Token oben im selben Raum sind
(so wie der RVS sie sieht). Die Art erkennt der RVS an der Vorstellung der
Clients — „unbekannt" ist ein Programm, das sich (noch) nicht vorgestellt hat.
</div>
<div class="card" style="max-width:900px;">
<div id="room-clients" style="font-size:12px;color:#8888AA;">(Aktualisieren klicken)</div>
</div>
</div>
<!-- App-Onboarding via QR-Code -->
<div class="settings-section">
<h2>App-Onboarding (QR-Code)</h2>
@@ -2573,6 +2589,7 @@
return;
}
if (msg.type === 'host_update') { hosts = msg.hosts || []; renderHosts(); return; }
if (msg.type === 'room_clients') { renderRoomClients(msg.payload || {}); return; }
if (msg.type === 'rooms_info') { rooms = (msg.payload && msg.payload.rooms) || []; renderRooms(); return; }
if (msg.type === 'sat_result') { onSatResult(msg.payload || {}); return; }
if (msg.type === 'llm_openai_config_result') { onOpenAiConfigResult(msg.payload || {}); return; }
@@ -5134,6 +5151,7 @@
if (tab === 'settings') {
send({ action: 'get_voice_config' });
loadRuntimeConfig();
requestRoomClients();
loadOnboardingQR();
loadOAuthServices();
loadJellyfinConfig();
@@ -5359,6 +5377,52 @@
function requestSatCreds(id) {
send({ action: 'sat_creds_list', satellite: id });
}
// ── Wer ist im Raum? ─────────────────────────
function requestRoomClients() {
const box = document.getElementById('room-clients');
if (box && !box.dataset.loaded) box.textContent = 'Frage den RVS …';
send({ action: 'room_clients_query' });
}
function _ago(ms) {
if (!ms) return '–';
const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
if (s < 60) return s + ' s';
if (s < 3600) return Math.round(s / 60) + ' min';
if (s < 86400) return Math.round(s / 3600) + ' h';
return Math.round(s / 86400) + ' d';
}
const ROOM_KIND_ICON = { 'Bridge': '🌉', 'Diagnostic': '🩺', 'App': '📱', 'Satellit': '🛰️',
'Host-Agent': '🖥️', 'KI-Box': '🧠', 'LLM-Client': '🤖' };
function renderRoomClients(p) {
const box = document.getElementById('room-clients');
if (!box) return;
box.dataset.loaded = '1';
if (p.error) { box.innerHTML = '<span style="color:#FF6B6B;">' + escapeHtml(p.error) + '</span>'; return; }
const list = p.clients || [];
if (!list.length) { box.innerHTML = '<span style="color:#8888AA;">(niemand — oder der RVS kennt die Abfrage noch nicht: RVS aktualisieren)</span>'; return; }
const td = 'padding:5px 8px;border-top:1px solid #1E1E2E;vertical-align:top;';
box.innerHTML = '<div style="color:#E0E0F0;margin-bottom:6px;">' + list.length + ' Verbindung' + (list.length === 1 ? '' : 'en') + '</div>' +
'<div style="overflow-x:auto;"><table style="width:100%;border-collapse:collapse;font-size:12px;">' +
'<tr style="color:#8888AA;text-align:left;"><th style="padding:4px 8px;">Art</th><th style="padding:4px 8px;">Name</th>' +
'<th style="padding:4px 8px;">Details</th><th style="padding:4px 8px;">Adresse</th>' +
'<th style="padding:4px 8px;">verbunden seit</th><th style="padding:4px 8px;">zuletzt aktiv</th></tr>' +
list.map(c => {
const kind = c.kind || '';
const icon = ROOM_KIND_ICON[kind] || '❔';
const kindTxt = kind ? escapeHtml(kind) : '<span style="color:#8888AA;">unbekannt</span>';
const you = c.you ? ' <span style="color:#0096FF;">★ diese Diagnostic</span>' : '';
const details = [c.detail, c.version ? 'v' + c.version : ''].filter(Boolean).join(' · ')
|| (!kind && c.userAgent ? c.userAgent : '');
return '<tr>' +
'<td style="' + td + 'white-space:nowrap;">' + icon + ' ' + kindTxt + '</td>' +
'<td style="' + td + 'color:#E0E0F0;">' + escapeHtml(c.name || '–') + you + '</td>' +
'<td style="' + td + 'color:#8888AA;">' + escapeHtml(details || '–') + '</td>' +
'<td style="' + td + 'color:#8888AA;font-family:monospace;">' + escapeHtml(c.ip || '–') + '</td>' +
'<td style="' + td + 'color:#8888AA;white-space:nowrap;">' + _ago(c.connectedAt) + '</td>' +
'<td style="' + td + 'color:#8888AA;white-space:nowrap;">' + _ago(c.lastSeen) + '</td></tr>';
}).join('') + '</table></div>';
}
let rooms = []; // [{token8, fp, len, clients, you}]
function requestRooms() { send({ action: 'rooms_query' }); }
function renderRooms() {
+16
View File
@@ -1216,6 +1216,12 @@ function connectRVS(forcePlain) {
state.rvs.lastError = null;
rvsWs = ws;
broadcastState();
// Beim RVS vorstellen (Liste "Wer ist im Raum?")
try {
ws.send(JSON.stringify({ type: "client_hello", payload: {
kind: "Diagnostic", name: require("os").hostname(), detail: "ARIA-Diagnostic" },
timestamp: Date.now() }));
} catch (_) {}
// Keepalive: alle 25s ein Ping senden damit die Verbindung nicht stirbt
const keepalive = setInterval(() => {
@@ -1294,6 +1300,9 @@ function connectRVS(forcePlain) {
} else if (msg.type === "host_ping") {
const p = msg.payload || {};
if (p.hostId && hosts.has(p.hostId)) hosts.get(p.hostId).last_seen = Date.now();
} else if (msg.type === "room_clients") {
// Antwort des RVS: wer ist in unserem Raum → an den Browser
broadcast({ type: "room_clients", payload: msg.payload || {} });
} else if (msg.type === "rooms_info") {
// Antwort des RVS auf rooms_query → an den Browser (Raum-Diagnose).
broadcast({ type: "rooms_info", payload: msg.payload || {} });
@@ -3181,6 +3190,13 @@ wss.on("connection", (ws) => {
log("info", "host", `Agent '${id}' aus der Liste entfernt`);
broadcastHosts();
}
} else if (msg.action === "room_clients_query") {
// Nur ueber die feste Verbindung — die Antwort kommt auf ihr zurueck
if (rvsWs && rvsWs.readyState === WebSocket.OPEN) {
rvsWs.send(JSON.stringify({ type: "room_clients_query", payload: {}, timestamp: Date.now() }));
} else {
ws.send(JSON.stringify({ type: "room_clients", payload: { error: "Diagnostic ist nicht mit dem RVS verbunden", clients: [] } }));
}
} else if (msg.action === "rooms_query") {
// Browser will die RVS-Raum-Diagnose — via persistente rvsWs anfragen,
// die Antwort (rooms_info) kommt auf derselben Verbindung zurueck.