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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user