feat(diagnostic): Satelliten-Tab — verbundene Satelliten + erkannte Geraete

Das Diagnostic haengt selbst am RVS-Raum und kennt die sat_*-Broadcasts jetzt:
- server.js: Satelliten-Registry (sat_hello), leitet sat_devices an den Browser,
  gibt die Liste beim init mit; Browser-Aktionen sat_list + sat_discover.
- index.html: neuer Haupt-Tab "Satelliten" — Karten pro Satellit (online/offline,
  Standort, Capabilities, read-only/steuerbar) mit "Geraete scannen" → Live-Liste
  der erkannten Geraete (Typ/IP/Modell/DIAL/MAC).

Kein Brain/Bridge-Rebuild fuer diesen Teil noetig (nur diagnostic). node -c OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 19:11:14 +02:00
co-authored by Claude Opus 4.8
parent 071e38f464
commit 1dc8c0936f
3 changed files with 140 additions and 0 deletions
+1
View File
@@ -24,6 +24,7 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
- RVS: neue Message-Typen `sat_hello / sat_discover / sat_devices / sat_command / sat_result`. - RVS: neue Message-Typen `sat_hello / sat_discover / sat_devices / sat_command / sat_result`.
- Bridge: Satelliten-Registry (`sat_hello`) + Future-Relay (`/internal/satellite`, `/internal/satellite-list`) analog zum flux-Muster. - Bridge: Satelliten-Registry (`sat_hello`) + Future-Relay (`/internal/satellite`, `/internal/satellite-list`) analog zum flux-Muster.
- Brain: Tools `satellite_list`, `satellite_devices`, `satellite_command` + Seed-Regel, die ARIA den Ablauf beibringt (erst list, dann devices, dann command). - Brain: Tools `satellite_list`, `satellite_devices`, `satellite_command` + Seed-Regel, die ARIA den Ablauf beibringt (erst list, dann devices, dann command).
- **Diagnostic: neuer Tab „Satelliten"** — zeigt live welche Satelliten verbunden sind (online/offline, Standort, Capabilities, read-only vs. steuerbar) und pro Satellit ein „Geräte scannen" (löst `sat_discover` aus → erkannte Geräte mit Typ/IP/Modell/DIAL/MAC).
### Deploy ### Deploy
Satellit im Ziel-Netz: `cd satellite && cp .env.example .env && docker compose up -d --build`. Haupt-Stack: `git pull && docker compose up -d --build brain bridge` + RVS-Stack `up -d --build`. Kein APK-Rebuild. Satellit im Ziel-Netz: `cd satellite && cp .env.example .env && docker compose up -d --build`. Haupt-Stack: `git pull && docker compose up -d --build brain bridge` + RVS-Stack `up -d --build`. Kein APK-Rebuild.
+98
View File
@@ -229,6 +229,7 @@
<button class="main-nav-btn" onclick="switchMainTab('skills')">Skills</button> <button class="main-nav-btn" onclick="switchMainTab('skills')">Skills</button>
<button class="main-nav-btn" onclick="switchMainTab('triggers')">Trigger</button> <button class="main-nav-btn" onclick="switchMainTab('triggers')">Trigger</button>
<button class="main-nav-btn" onclick="switchMainTab('files')">Dateien</button> <button class="main-nav-btn" onclick="switchMainTab('files')">Dateien</button>
<button class="main-nav-btn" onclick="switchMainTab('satellites')">Satelliten</button>
<button class="main-nav-btn" onclick="switchMainTab('settings')">Einstellungen</button> <button class="main-nav-btn" onclick="switchMainTab('settings')">Einstellungen</button>
</div> </div>
@@ -1271,6 +1272,27 @@
</div> </div>
</div><!-- /tab-triggers --> </div><!-- /tab-triggers -->
<!-- ══════ 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;">Satelliten 🛰️</h2>
<button class="btn secondary" onclick="requestSatellites()" 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;">
Aussenposten-Container in fremden Netzen (Buero, Werkstatt …), die sich mit
RVS verbinden. Hier siehst Du, welche Satelliten online sind und welche
Geraete sie im jeweiligen Netz erkannt haben. „Geraete scannen" fragt den
Satelliten live ab.
</p>
</div>
<div class="card">
<div id="sat-list" style="font-size:12px;color:#8888AA;">(Lade...)</div>
</div>
</div>
</div><!-- /tab-satellites -->
<!-- Trigger-Create/Edit Modal --> <!-- Trigger-Create/Edit Modal -->
<div class="modal-overlay" id="trigger-modal"> <div class="modal-overlay" id="trigger-modal">
<div class="modal-box" style="max-width:600px;"> <div class="modal-box" style="max-width:600px;">
@@ -1862,6 +1884,14 @@
return; return;
} }
if (msg.type === 'sat_update') { satellites = msg.satellites || []; renderSatellites(); return; }
if (msg.type === 'sat_devices') {
if (msg.satellite) { satDevices[msg.satellite] = { devices: msg.devices || [], location: msg.location, ts: Date.now() }; }
satScanning = null;
renderSatellites();
return;
}
if (msg.type === 'voice_id_delete_response') { if (msg.type === 'voice_id_delete_response') {
const p = msg.payload || msg; const p = msg.payload || msg;
if (p.removed) { if (p.removed) {
@@ -4045,9 +4075,77 @@
loadSkills(); loadSkills();
} else if (tab === 'triggers') { } else if (tab === 'triggers') {
loadTriggers(); loadTriggers();
} else if (tab === 'satellites') {
requestSatellites();
} }
} }
// ── Satelliten-Ansicht ─────────────────────────────────
let satellites = []; // [{id, location, caps, control, online}]
let satDevices = {}; // id → {devices, location, ts}
let satScanning = null; // id des gerade scannenden Satelliten
function requestSatellites() {
send({ action: 'sat_list' });
}
function scanSatellite(id) {
satScanning = id;
renderSatellites();
send({ action: 'sat_discover', satellite: id });
}
function renderSatellites() {
const box = document.getElementById('sat-list');
if (!box) return;
if (!satellites.length) {
box.innerHTML = '<span style="color:#8888AA;">Kein Satellit verbunden. ' +
'Deploy einen satellite/-Container in einem Netz (siehe satellite/README).</span>';
return;
}
box.innerHTML = satellites.map(s => {
const online = s.online !== false;
const dot = online ? '#34C759' : '#FF3B30';
const caps = (s.caps || []).join(', ') || 'nur beobachten';
const ctrl = s.control ? '<span style="color:#0096FF;">steuerbar</span>' : '<span style="color:#8888AA;">read-only</span>';
const dev = satDevices[s.id];
const scanning = satScanning === s.id;
let devHtml;
if (scanning) {
devHtml = '<div style="color:#FFD60A;margin-top:6px;">Scanne Netz …</div>';
} else if (dev) {
if (!dev.devices.length) {
devHtml = '<div style="color:#8888AA;margin-top:6px;">Keine Geraete gefunden.</div>';
} else {
devHtml = '<div style="margin-top:6px;display:flex;flex-direction:column;gap:3px;">' +
dev.devices.map(d => {
const tags = [];
if (d.model) tags.push(escapeHtml(d.model));
if (d.dialAppUrl) tags.push('DIAL');
if (d.mac) tags.push(escapeHtml(d.mac));
const tagStr = tags.length ? ' <span style="color:#666;">(' + tags.join(' · ') + ')</span>' : '';
return '<div style="padding:3px 6px;background:#0D0D1A;border-radius:4px;">' +
'<span style="color:#E0E0F0;">' + escapeHtml(d.name || d.id || '?') + '</span> ' +
'<span style="color:#0096FF;">[' + escapeHtml(d.type || '?') + ']</span> ' +
'<span style="color:#8888AA;">' + escapeHtml(d.ip || '') + '</span>' + tagStr +
'</div>';
}).join('') + '</div>';
}
} else {
devHtml = '<div style="color:#8888AA;margin-top:6px;">Noch nicht gescannt.</div>';
}
return '<div style="border:1px solid #1E1E2E;border-radius:8px;padding:10px;margin-bottom:8px;">' +
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
'<div><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + dot + ';margin-right:6px;"></span>' +
'<strong style="color:#E0E0F0;">' + escapeHtml(s.location || s.id) + '</strong> ' +
'<span style="color:#666;font-size:11px;">id=' + escapeHtml(s.id) + '</span></div>' +
'<button class="btn secondary" onclick="scanSatellite(\'' + escapeHtml(s.id) + '\')" style="padding:3px 8px;font-size:11px;"' +
(scanning ? ' disabled' : '') + '>Geraete scannen</button>' +
'</div>' +
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">kann: ' + escapeHtml(caps) + ' · ' + ctrl + '</div>' +
devHtml +
'</div>';
}).join('');
}
// ── Triggers-Verwaltung ──────────────────────────────── // ── Triggers-Verwaltung ────────────────────────────────
let triggersCache = []; let triggersCache = [];
+41
View File
@@ -475,6 +475,21 @@ function broadcastState() {
broadcast({ type: "state", state }); broadcast({ type: "state", state });
} }
// ── Satelliten-Registry (Aussenposten in fremden Netzen) ──────────
const satellites = new Map(); // id → {id, location, caps, control, last_seen}
function satelliteList() {
const now = Date.now();
return Array.from(satellites.values()).map(s => ({
id: s.id, location: s.location, caps: s.caps, control: s.control,
online: (now - (s.last_seen || 0)) < 300000,
}));
}
function broadcastSatellites() {
broadcast({ type: "sat_update", satellites: satelliteList() });
}
// ── OpenClaw Gateway Verbindung ───────────────────────── // ── OpenClaw Gateway Verbindung ─────────────────────────
async function connectGateway() { async function connectGateway() {
@@ -903,6 +918,22 @@ function connectRVS(forcePlain) {
// Browser-Tabs weiterreichen, damit die Projektliste live neu laedt // Browser-Tabs weiterreichen, damit die Projektliste live neu laedt
// (bisher wurde das NICHT geforwardet → Diagnostic aktualisierte nie). // (bisher wurde das NICHT geforwardet → Diagnostic aktualisierte nie).
broadcast({ type: "project_changed", payload: msg.payload || {} }); broadcast({ type: "project_changed", payload: msg.payload || {} });
} else if (msg.type === "sat_hello") {
// Ein Satellit (Aussenposten in einem fremden Netz) meldet sich.
const p = msg.payload || {};
if (p.id) {
satellites.set(p.id, {
id: p.id, location: p.location || p.id,
caps: p.caps || [], control: !!p.control, last_seen: Date.now(),
});
broadcastSatellites();
}
} else if (msg.type === "sat_devices") {
// Antwort eines Satelliten auf sat_discover → Geraeteliste an Browser.
const p = msg.payload || {};
if (p.satellite && satellites.has(p.satellite)) satellites.get(p.satellite).last_seen = Date.now();
broadcast({ type: "sat_devices", satellite: p.satellite || "",
location: p.location || "", devices: p.devices || [] });
} else if (msg.type === "agent_activity") { } else if (msg.type === "agent_activity") {
// Bridge meldet "ARIA denkt/schreibt/tool" oder "idle" — an Browser // Bridge meldet "ARIA denkt/schreibt/tool" oder "idle" — an Browser
// weiterreichen, damit der Thinking-Indikator im Chat erscheint. // weiterreichen, damit der Thinking-Indikator im Chat erscheint.
@@ -2407,6 +2438,8 @@ wss.on("connection", (ws) => {
ws.send(JSON.stringify({ type: "init", state, logs: logs.slice(-100) })); ws.send(JSON.stringify({ type: "init", state, logs: logs.slice(-100) }));
// Letzten Disk-Status mitgeben damit der Client sofort weiss wie's um Platz steht // Letzten Disk-Status mitgeben damit der Client sofort weiss wie's um Platz steht
if (currentDiskStatus) ws.send(JSON.stringify(currentDiskStatus)); if (currentDiskStatus) ws.send(JSON.stringify(currentDiskStatus));
// Aktuell bekannte Satelliten mitgeben (RVS replayt sat_hello nicht).
ws.send(JSON.stringify({ type: "sat_update", satellites: satelliteList() }));
ws.on("message", (raw) => { ws.on("message", (raw) => {
try { try {
@@ -2425,6 +2458,14 @@ wss.on("connection", (ws) => {
connectGateway(); connectGateway();
} else if (msg.action === "reconnect_rvs") { } else if (msg.action === "reconnect_rvs") {
connectRVS(); connectRVS();
} else if (msg.action === "sat_list") {
// Browser will die aktuelle Satelliten-Liste.
ws.send(JSON.stringify({ type: "sat_update", satellites: satelliteList() }));
} else if (msg.action === "sat_discover") {
// Browser triggert einen Geraete-Scan auf einem Satelliten.
sendToRVS_raw({ type: "sat_discover",
payload: { satellite: msg.satellite || "", force: true },
timestamp: Date.now() });
} else if (msg.action === "test_proxy") { } else if (msg.action === "test_proxy") {
testProxy(msg.text); testProxy(msg.text);
} else if (msg.action === "check_proxy_auth") { } else if (msg.action === "check_proxy_auth") {