From 7b4a1f4429d7298618f31e0b824a6cf76cb8c05b Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 25 Oct 2026 12:34:14 +0100 Subject: [PATCH] feat(diagnostic): Liste, wer gerade mit dem RVS-Raum verbunden ist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- android/src/services/rvs.ts | 15 ++++++- bridge/aria_bridge.py | 7 +++ diagnostic/index.html | 64 +++++++++++++++++++++++++++ diagnostic/server.js | 16 +++++++ rvs/server.js | 87 +++++++++++++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 1 deletion(-) diff --git a/android/src/services/rvs.ts b/android/src/services/rvs.ts index 9b188c6..324a730 100644 --- a/android/src/services/rvs.ts +++ b/android/src/services/rvs.ts @@ -7,7 +7,7 @@ */ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { NativeModules } from 'react-native'; +import { NativeModules, Platform } from 'react-native'; const TlsTrust: any = NativeModules.TlsTrust; @@ -247,6 +247,7 @@ class RVSConnection { this.log('info', `Verbunden${tlsInfo}`); this.setState('connected'); this.updateSecurityOnOpen(useTLS); + this.sendClientHello(); this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS; this.startHeartbeat(); }; @@ -323,6 +324,18 @@ class RVSConnection { } } + /** Beim RVS vorstellen — Diagnostic zeigt so, welches Handy im Raum ist. */ + private sendClientHello(): void { + try { + const c: any = (Platform as any).constants || {}; + const device = [c.Manufacturer, c.Model].filter(Boolean).join(' ') || Platform.OS; + let version = ''; + try { version = require('../../package.json').version || ''; } catch (_) {} + this.send('client_hello', { kind: 'App', name: device, + detail: `Android ${c.Release || Platform.Version}`, version }); + } catch (_) {} + } + // --- Verbindungs-Sicherheit (Schloss-Symbol) --- getSecurity(): SecurityState { diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 302e81e..9e68683 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -2272,6 +2272,13 @@ class ARIABridge: self.ws_rvs = ws retry_delay = 2 logger.info("[rvs] Verbunden — warte auf App-Nachrichten") + # Beim RVS vorstellen (Diagnostic → "Wer ist im Raum?") + try: + await ws.send(json.dumps({"type": "client_hello", "payload": { + "kind": "Bridge", "name": socket.gethostname(), + "detail": "ARIA-Bridge"}, "timestamp": int(time.time() * 1000)})) + except Exception: + pass # TCP-Keepalive auf dem unterliegenden Socket aktivieren — # damit NAT-Tabellen-Verfall oder "halb-tote" Verbindungen diff --git a/diagnostic/index.html b/diagnostic/index.html index 66423d8..224f2d1 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -1388,6 +1388,22 @@ + +
+
+

Verbunden mit diesem Raum

+ +
+
+ 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. +
+
+
(Aktualisieren klicken)
+
+
+

App-Onboarding (QR-Code)

@@ -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 = '' + escapeHtml(p.error) + ''; return; } + const list = p.clients || []; + if (!list.length) { box.innerHTML = '(niemand — oder der RVS kennt die Abfrage noch nicht: RVS aktualisieren)'; return; } + const td = 'padding:5px 8px;border-top:1px solid #1E1E2E;vertical-align:top;'; + box.innerHTML = '
' + list.length + ' Verbindung' + (list.length === 1 ? '' : 'en') + '
' + + '
' + + '' + + '' + + '' + + list.map(c => { + const kind = c.kind || ''; + const icon = ROOM_KIND_ICON[kind] || '❔'; + const kindTxt = kind ? escapeHtml(kind) : 'unbekannt'; + const you = c.you ? ' ★ diese Diagnostic' : ''; + const details = [c.detail, c.version ? 'v' + c.version : ''].filter(Boolean).join(' · ') + || (!kind && c.userAgent ? c.userAgent : ''); + return '' + + '' + + '' + + '' + + '' + + '' + + ''; + }).join('') + '
ArtNameDetailsAdresseverbunden seitzuletzt aktiv
' + icon + ' ' + kindTxt + '' + escapeHtml(c.name || '–') + you + '' + escapeHtml(details || '–') + '' + escapeHtml(c.ip || '–') + '' + _ago(c.connectedAt) + '' + _ago(c.lastSeen) + '
'; + } + let rooms = []; // [{token8, fp, len, clients, you}] function requestRooms() { send({ action: 'rooms_query' }); } function renderRooms() { diff --git a/diagnostic/server.js b/diagnostic/server.js index 84c2942..889fc30 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -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. diff --git a/rvs/server.js b/rvs/server.js index bdc5a73..b2a67fc 100644 --- a/rvs/server.js +++ b/rvs/server.js @@ -145,6 +145,9 @@ const ALLOWED_TYPES = new Set([ // (Last, Ausfall-Ersatz) und antwortet. Empfohlener Weg fuer Programme. "llm_cluster_models_request", "llm_cluster_models", "llm_cluster_request", "llm_cluster_response", + // Wer ist im Raum? client_hello: Client stellt sich vor (bleibt beim RVS); + // room_clients_query → room_clients: Liste NUR des eigenen Raums. + "client_hello", "room_clients_query", "room_clients", "node_stats", "node_stats_stream_start", "node_stats_stream_stop", "node_stats_history_request", "node_stats_history", "node_stats_reset", "node_stats_reset_done", @@ -422,6 +425,11 @@ function escapeHtml(s) { } wss.on("connection", (ws, req) => { + // Fuer die Raum-Liste: Herkunft (hinter einem Reverse-Proxy steht die echte + // Adresse in X-Forwarded-For) und Programm-Kennung + const xff = String(req.headers["x-forwarded-for"] || "").split(",")[0].trim(); + ws._ip = (xff || req.socket.remoteAddress || "").replace(/^::ffff:/, ""); + ws._ua = String(req.headers["user-agent"] || "").slice(0, 120); // Token aus URL-Query lesen: ws://host:port/?token=abc123 const url = new URL(req.url, `http://${req.headers.host}`); let token = url.searchParams.get("token"); @@ -446,6 +454,70 @@ wss.on("connection", (ws, req) => { registerClient(ws, token); }); +/** Wer steckt hinter einer Verbindung? Aus Nachrichten, die ohnehin fliessen + * (sat_hello, host_hello, worker_hello, update_check der App, Cluster- + * Anfragen) oder aus einem expliziten client_hello. Nur zur Anzeige. */ +function learnIdentity(ws, msg) { + const p = msg.payload || {}; + const id = ws._ident; + const set = (kind, name, detail, version) => { + // client_hello gewinnt; abgeleitete Angaben ueberschreiben es nicht + if (id.explicit && msg.type !== "client_hello") return; + id.kind = kind; + id.name = String(name || "").slice(0, 80); + id.detail = String(detail || "").slice(0, 120); + if (version) id.version = String(version).slice(0, 40); + }; + switch (msg.type) { + case "client_hello": + id.explicit = true; + set(p.kind || "Client", p.name, p.detail, p.version); + break; + case "sat_hello": + set("Satellit", p.id, p.location, ""); + break; + case "host_hello": + set("Host-Agent", p.name || p.hostId, p.os, p.version); + break; + case "worker_hello": { + const svc = p.service || "?"; + if (!id.services) id.services = new Set(); + id.services.add(svc); + set("KI-Box", p.node || p.instanceId, [...id.services].join(", ") + (p.gpus ? ` · GPU ${p.gpus}` : ""), ""); + break; + } + case "update_check": + if (!id.kind) set("App", "", "", p.version); + else if (p.version && !id.version) id.version = String(p.version).slice(0, 40); + break; + case "llm_cluster_request": + case "llm_cluster_models_request": + if (!id.kind) { + const rid = String(p.requestId || ""); + set("LLM-Client", rid.includes("-") ? rid.split("-")[0] : "", "", ""); + } + break; + } +} + +function roomClients(ws) { + const room = rooms.get(ws._token); + const out = []; + if (!room) return out; + for (const c of room.clients) { + if (c.readyState !== 1) continue; + const id = c._ident || {}; + out.push({ + kind: id.kind || "", name: id.name || "", detail: id.detail || "", + version: id.version || "", ip: c._ip || "", userAgent: c._ua || "", + connectedAt: c._connectedAt || 0, lastSeen: c._lastSeen || 0, + you: c === ws, + }); + } + out.sort((a, b) => (a.kind || "~").localeCompare(b.kind || "~") || a.name.localeCompare(b.name)); + return out; +} + function registerClient(ws, token) { // Maximale Anzahl aktiver Sessions prüfen if (!rooms.has(token) && rooms.size >= MAX_SESSIONS) { @@ -463,6 +535,9 @@ function registerClient(ws, token) { const room = rooms.get(token); room.clients.add(ws); ws._token = token; + ws._ident = {}; + ws._connectedAt = Date.now(); + ws._lastSeen = ws._connectedAt; log(`Client verbunden: ${token.slice(0, 8)}... (${room.clients.size} im Raum)`); @@ -479,6 +554,18 @@ function registerClient(ws, token) { if (!msg.type || !ALLOWED_TYPES.has(msg.type)) { return; } + ws._lastSeen = Date.now(); + try { learnIdentity(ws, msg); } catch (_) {} + + // Vorstellung bleibt beim RVS, Raum-Liste nur an den Fragenden + if (msg.type === "client_hello") return; + if (msg.type === "room_clients_query") { + ws.send(JSON.stringify({ type: "room_clients", payload: { + requestId: (msg.payload && msg.payload.requestId) || "", clients: roomClients(ws), + }, timestamp: Date.now() })); + return; + } + if (msg.type === "room_clients") return; // kommt nur vom RVS // Update-Check: direkt an den anfragenden Client antworten (nicht relay'en) if (msg.type === "update_check") {