feat(diagnostic): RVS-Raum-Monitor + Server/GPU-Icon fuer die Compute-Flotte

Raum-Monitor (haette uns die Nacht erspart): der RVS-Log kuerzt Tokens auf 8
Zeichen -> zwei verschiedene Tokens mit gleichem Prefix ergeben zwei Raeume,
die im Log IDENTISCH aussehen (Prefix-Kollision). Jetzt zeigt die Diagnostic
(Satelliten-Tab, Panel 'RVS-Raeume 🚪') alle Raeume mit token8 + laengerem
Fingerprint (sha256) + Token-Laenge + Client-Zahl, ★ = eigener Raum. >1 Raum =
Warnung 'Token-Mismatch'. Keine vollen Tokens (nur Fingerprint).

- rvs: rooms_query -> direkt rooms_info antworten (wie update_check); Typen in
  ALLOWED_TYPES; sha256-Fingerprint via crypto.
- diagnostic: rooms_query durchreichen, rooms_info an Browser; Panel + Tabelle.

Dazu: Compute-Flotte-Ueberschrift bekommt statt 🖥️ ein Inline-SVG (Server mit
GPU-Karte, currentColor -> theme-aware).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 18:29:37 +02:00
co-authored by Claude Opus 4.8
parent 6cb29a28ce
commit 145c64e67f
3 changed files with 87 additions and 1 deletions
+53 -1
View File
@@ -1451,6 +1451,23 @@
<!-- ══════ TAB: Satelliten ══════ -->
<div id="tab-satellites" class="main-tab">
<div class="settings-section">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2 style="margin:0;">RVS-Räume 🚪</h2>
<button class="btn secondary" onclick="requestRooms()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
</div>
<div class="card" style="margin-bottom:8px;">
<p style="color:#8888AA;font-size:12px;margin:0;">
Alle Räume auf dem RVS. Jeder Token bildet einen eigenen Raum — nur Clients
im GLEICHEN Raum sehen sich. Der RVS-Log kürzt Tokens auf 8 Zeichen, dadurch
können zwei verschiedene Tokens gleich aussehen (Prefix-Kollision). Der
<strong>Fingerprint</strong> + die <strong>Länge</strong> hier machen das
eindeutig: <em>mehrere Räume = Token-Mismatch irgendwo</em>. ★ = dein Raum
(Diagnostic).
</p>
</div>
<div class="card" style="margin-bottom:16px;">
<div id="rooms-list" style="font-size:12px;color:#8888AA;">(Aktualisieren klicken)</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2 style="margin:0;">Satelliten 🛰️</h2>
<button class="btn secondary" onclick="requestSatellites()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
@@ -1484,7 +1501,19 @@
</div>
<div id="compute-fleet" style="display:flex;justify-content:space-between;align-items:center;margin:16px 0 8px;scroll-margin-top:70px;">
<h2 style="margin:0;">Compute-Flotte 🖥️</h2>
<h2 style="margin:0;display:flex;align-items:center;gap:7px;">Compute-Flotte
<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true" style="flex:none;">
<!-- Server -->
<rect x="3" y="3" width="13" height="16" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="6" y1="6.5" x2="13" y2="6.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<line x1="6" y1="9.5" x2="13" y2="9.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<circle cx="6.3" cy="15.5" r="0.95" fill="currentColor"/>
<!-- GPU-Karte (Badge unten rechts, zwei Luefter) -->
<rect x="11.5" y="12.5" width="10" height="7" rx="1.2" fill="currentColor" fill-opacity="0.15" stroke="currentColor" stroke-width="1.3"/>
<circle cx="14.5" cy="16" r="1.6" fill="none" stroke="currentColor" stroke-width="1.1"/>
<circle cx="18.4" cy="16" r="1.6" fill="none" stroke="currentColor" stroke-width="1.1"/>
</svg>
</h2>
<button class="btn secondary" onclick="requestWorkers()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
</div>
<div class="card" style="margin-bottom:8px;">
@@ -2155,6 +2184,7 @@
return;
}
if (msg.type === 'host_update') { hosts = msg.hosts || []; renderHosts(); return; }
if (msg.type === 'rooms_info') { rooms = (msg.payload && msg.payload.rooms) || []; renderRooms(); return; }
if (msg.type === 'sat_creds_list_result') {
const p = msg.payload || msg;
const sat = p.satellite || '';
@@ -4442,6 +4472,7 @@
} else if (tab === 'triggers') {
loadTriggers();
} else if (tab === 'satellites') {
requestRooms();
requestSatellites();
requestHosts();
requestWorkers();
@@ -4575,6 +4606,27 @@
function requestSatCreds(id) {
send({ action: 'sat_creds_list', satellite: id });
}
let rooms = []; // [{token8, fp, len, clients, you}]
function requestRooms() { send({ action: 'rooms_query' }); }
function renderRooms() {
const box = document.getElementById('rooms-list');
if (!box) return;
if (!rooms.length) { box.innerHTML = '<span style="color:#8888AA;">(keine Räume — RVS leer?)</span>'; return; }
const warn = rooms.length > 1
? '<div style="color:#FFB020;margin-bottom:8px;">⚠ ' + rooms.length + ' Räume — Clients in verschiedenen Räumen sehen sich NICHT. Meist ein abweichender/abgeschnittener RVS_TOKEN.</div>'
: '<div style="color:#3FFF3F;margin-bottom:8px;">✓ Genau 1 Raum — alle Clients zusammen.</div>';
box.innerHTML = warn + '<table style="width:100%;border-collapse:collapse;font-size:12px;">' +
'<tr style="color:#8888AA;text-align:left;"><th>Raum (token8)</th><th>Fingerprint</th><th>Länge</th><th>Clients</th></tr>' +
rooms.map(r => {
const you = r.you ? ' <span style="color:#0096FF;">★ du</span>' : '';
const c = rooms.length > 1 ? (r.you ? '#3FFF3F' : '#FFB020') : '#E0E0F0';
return '<tr style="border-top:1px solid #1E1E2E;">' +
'<td style="color:' + c + ';font-family:monospace;">' + escapeHtml(r.token8) + '…' + you + '</td>' +
'<td style="color:#8888AA;font-family:monospace;">' + escapeHtml(r.fp) + '</td>' +
'<td style="color:#8888AA;">' + r.len + '</td>' +
'<td style="color:#E0E0F0;">' + r.clients + '</td></tr>';
}).join('') + '</table>';
}
let hosts = []; // [{hostId, name, os, caps, control, online}]
function requestHosts() { send({ action: 'host_list' }); }
function renderHosts() {
+7
View File
@@ -1095,6 +1095,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 === "rooms_info") {
// Antwort des RVS auf rooms_query → an den Browser (Raum-Diagnose).
broadcast({ type: "rooms_info", payload: msg.payload || {} });
} else if (msg.type === "sat_devices") {
// Antwort eines Satelliten auf sat_discover → Geraeteliste an Browser.
const p = msg.payload || {};
@@ -2789,6 +2792,10 @@ wss.on("connection", (ws) => {
} else if (msg.action === "host_list") {
// Browser will die aktuelle Host-Agenten-Liste.
ws.send(JSON.stringify({ type: "host_update", hosts: hostList() }));
} 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.
sendToRVS_raw({ type: "rooms_query", payload: {}, timestamp: Date.now() });
} else if (msg.action === "worker_list") {
// Browser will die aktuelle Compute-Flotte.
ws.send(JSON.stringify({ type: "worker_update", workers: workerList() }));
+27
View File
@@ -88,6 +88,10 @@ const ALLOWED_TYPES = new Set([
// host_hello/host_ping und fuehrt host_command aus (exec/read/write/info/
// screenshot) -> host_result. ARIA steuert so Rechner auch hinter NAT.
"host_hello", "host_ping", "host_command", "host_result",
// Raum-Diagnose: Diagnostic fragt die aktuellen RVS-Raeume ab (rooms_query),
// RVS antwortet direkt mit rooms_info (Fingerprint + Laenge + Client-Zahl je
// Raum). Deckt Token-Prefix-Kollisionen auf (2 Raeume, gleicher 8-Zeichen-Log).
"rooms_query", "rooms_info",
// Compute-Flotte (AI-Boxen): Worker (f5tts/whisper/voxtral/llm-adapter) melden
// sich per worker_hello an und pingen per worker_ping; der Diagnostic-Server
// aggregiert das und broadcastet worker_update/worker_list an die Browser-UI.
@@ -349,6 +353,29 @@ function registerClient(ws, token) {
return;
}
// Raum-Diagnose: direkt an den anfragenden Client antworten (nicht relay'en).
// Zeigt ALLE Raeume mit token8 (wie im Log), einem laengeren Fingerprint und
// der Token-Laenge — so werden Prefix-Kollisionen (2 Raeume, gleicher 8-Zeichen-
// Log, aber verschiedene volle Tokens) sofort sichtbar. KEINE vollen Tokens.
if (msg.type === "rooms_query") {
const crypto = require("crypto");
const out = [];
for (const [tok, room] of rooms) {
let live = 0;
for (const c of room.clients) if (c.readyState === 1) live++;
out.push({
token8: tok.slice(0, 8),
fp: crypto.createHash("sha256").update(tok).digest("hex").slice(0, 12),
len: tok.length,
clients: live,
you: tok === ws._token,
});
}
out.sort((a, b) => b.clients - a.clients);
ws.send(JSON.stringify({ type: "rooms_info", payload: { rooms: out }, timestamp: Date.now() }));
return;
}
// Update-Download: APK als Base64 ueber WebSocket senden
if (msg.type === "update_download") {
const apkInfo = getLatestAPK();