feat(compute): Worker-Selbstanmeldung ueber RVS + Flotten-Anzeige (Stage 2)

Jeder GPU-Dienst meldet sich beim Connect mit worker_hello {instanceId,
service, node, gpus, model} und haelt die Registry per periodischem
worker_ping {instanceId, busy} (~10s) frisch. So weiss ARIA, was wo laeuft.

- xtts/{voxtral,whisper,f5tts}/bridge.py + llm-adapter/adapter.py:
  INSTANCE_ID=service@NODE_NAME, _worker_register()-Coroutine (hello + ping),
  busy-Quelle je Worker (aktive STT-Sessions / TTS-Render / in-flight LLM);
  Task sauber gecancelt bei Reconnect.
- bridge/aria_bridge.py: self._workers-Registry + Handler worker_hello/
  worker_ping (spiegelt sat_hello), _worker_list() (35s-Offline-TTL),
  _pick_worker() (Round-Robin freie Instanz, fuer Stage-3-Routing),
  /internal/worker-list-Endpoint.
- diagnostic/server.js: workers-Map, worker_hello/worker_ping-Tracking,
  worker_update-Broadcast + worker_list-Action + on-connect-Snapshot.
- diagnostic/index.html: "Compute-Flotte"-Panel im Satelliten-Tab — pro Node
  gruppiert, mit Dienst/Modell/GPU und frei/beschaeftigt/offline-Status.

Stage 2 von 3. Reine Sichtbarkeit, kein Routing-Verhalten geaendert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 12:22:47 +02:00
co-authored by Claude Opus 4.8
parent e75f1eeb6a
commit f2ead1242f
7 changed files with 330 additions and 0 deletions
+58
View File
@@ -1307,6 +1307,22 @@
<div class="card">
<div id="sat-list" style="font-size:12px;color:#8888AA;">(Lade...)</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin:16px 0 8px;">
<h2 style="margin:0;">Compute-Flotte 🖥️</h2>
<button class="btn secondary" onclick="requestWorkers()" 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;">
GPU-Dienste (STT / TTS / lokales LLM), verteilt auf einen oder mehrere
Nodes. Jeder Node startet per <code>COMPOSE_PROFILES</code> nur die
Dienste, die er anbietet. Hier siehst Du, welche Instanz auf welchem
Node laeuft, mit welchem Modell/GPU und ob sie gerade beschaeftigt ist.
</p>
</div>
<div class="card">
<div id="worker-list" style="font-size:12px;color:#8888AA;">(Lade...)</div>
</div>
</div>
</div><!-- /tab-satellites -->
@@ -1958,6 +1974,7 @@
}
if (msg.type === 'sat_update') { satellites = msg.satellites || []; renderSatellites(); return; }
if (msg.type === 'worker_update') { workers = msg.workers || []; renderWorkers(); return; }
if (msg.type === 'sat_devices') {
if (msg.satellite) { satDevices[msg.satellite] = { devices: msg.devices || [], location: msg.location, ts: Date.now() }; }
satScanning = null;
@@ -4179,9 +4196,50 @@
loadTriggers();
} else if (tab === 'satellites') {
requestSatellites();
requestWorkers();
}
}
// ── Compute-Flotte ─────────────────────────────────────
let workers = []; // [{instanceId, service, node, gpus, model, busy, online}]
const WORKER_SVC_META = {
voxtral: { icon: '🎙️', label: 'STT (Voxtral)' },
whisper: { icon: '🎙️', label: 'STT (Whisper)' },
f5tts: { icon: '🔊', label: 'TTS (F5-TTS)' },
llm: { icon: '🧠', label: 'LLM (lokal)' },
};
function requestWorkers() { send({ action: 'worker_list' }); }
function renderWorkers() {
const box = document.getElementById('worker-list');
if (!box) return;
if (!workers.length) {
box.innerHTML = '<span style="color:#8888AA;">Kein Compute-Node verbunden. ' +
'Starte auf einem GPU-Rechner <code>cd xtts &amp;&amp; docker compose up -d</code> ' +
'(mit gesetztem <code>COMPOSE_PROFILES</code>).</span>';
return;
}
// Nach Node gruppieren.
const byNode = {};
for (const w of workers) { (byNode[w.node || '?'] = byNode[w.node || '?'] || []).push(w); }
box.innerHTML = Object.keys(byNode).sort().map(node => {
const rows = byNode[node].map(w => {
const meta = WORKER_SVC_META[w.service] || { icon: '⚙️', label: w.service };
const dot = !w.online ? '#666' : (w.busy ? '#FFB020' : '#3FFF3F');
const stat = !w.online ? 'offline' : (w.busy ? 'beschaeftigt' : 'frei');
return '<div style="display:flex;align-items:center;gap:8px;padding:4px 0;">' +
'<span style="width:8px;height:8px;border-radius:50%;background:' + dot + ';display:inline-block;"></span>' +
'<span>' + meta.icon + ' <b>' + escapeHtml(meta.label) + '</b></span>' +
'<span style="color:#8888AA;">' + escapeHtml(w.model || '') + '</span>' +
(w.gpus ? '<span style="color:#8888AA;">GPU ' + escapeHtml(w.gpus) + '</span>' : '') +
'<span style="margin-left:auto;color:' + dot + ';">' + stat + '</span>' +
'</div>';
}).join('');
return '<div style="margin-bottom:10px;">' +
'<div style="font-weight:600;color:#AAB;margin-bottom:2px;">🖥️ ' + escapeHtml(node) + '</div>' +
rows + '</div>';
}).join('');
}
// ── Satelliten-Ansicht ─────────────────────────────────
let satellites = []; // [{id, location, caps, control, online}]
let satDevices = {}; // id → {devices, location, ts}
+49
View File
@@ -490,6 +490,24 @@ function broadcastSatellites() {
broadcast({ type: "sat_update", satellites: satelliteList() });
}
// ── Compute-Fleet: GPU-Worker (voxtral/whisper/f5tts/llm) ──────────
// Worker melden sich per worker_hello + halten sich per worker_ping (busy) frisch.
const workers = new Map(); // instanceId → {instanceId, service, node, gpus, model, busy, last_seen}
const WORKER_OFFLINE_MS = 35000; // ping ~10s; nach 35s ohne Ping = offline
function workerList() {
const now = Date.now();
return Array.from(workers.values()).map(w => ({
instanceId: w.instanceId, service: w.service, node: w.node,
gpus: w.gpus, model: w.model, busy: !!w.busy,
online: (now - (w.last_seen || 0)) < WORKER_OFFLINE_MS,
}));
}
function broadcastWorkers() {
broadcast({ type: "worker_update", workers: workerList() });
}
// ── OpenClaw Gateway Verbindung ─────────────────────────
async function connectGateway() {
@@ -935,6 +953,32 @@ function connectRVS(forcePlain) {
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 === "worker_hello") {
// Ein Compute-Worker (GPU-Dienst) meldet sich mit seiner Identitaet.
const p = msg.payload || {};
if (p.instanceId) {
const prev = workers.get(p.instanceId) || {};
workers.set(p.instanceId, {
instanceId: p.instanceId, service: p.service || "",
node: p.node || "", gpus: p.gpus || "", model: p.model || "",
busy: !!prev.busy, last_seen: Date.now(),
});
broadcastWorkers();
}
} else if (msg.type === "worker_ping") {
// Heartbeat eines Workers (traegt busy-Status).
const p = msg.payload || {};
if (p.instanceId) {
let w = workers.get(p.instanceId);
if (!w) {
const svc = String(p.instanceId).split("@")[0];
w = { instanceId: p.instanceId, service: svc, node: "", gpus: "", model: "", busy: false, last_seen: 0 };
workers.set(p.instanceId, w);
}
w.busy = !!p.busy;
w.last_seen = Date.now();
broadcastWorkers();
}
} else if (msg.type === "agent_activity") {
// Bridge meldet "ARIA denkt/schreibt/tool" oder "idle" — an Browser
// weiterreichen, damit der Thinking-Indikator im Chat erscheint.
@@ -2505,6 +2549,8 @@ wss.on("connection", (ws) => {
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() }));
// Aktuell bekannte Compute-Worker mitgeben (RVS replayt worker_hello nicht).
ws.send(JSON.stringify({ type: "worker_update", workers: workerList() }));
ws.on("message", (raw) => {
try {
@@ -2536,6 +2582,9 @@ wss.on("connection", (ws) => {
} 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 === "worker_list") {
// Browser will die aktuelle Compute-Flotte.
ws.send(JSON.stringify({ type: "worker_update", workers: workerList() }));
} else if (msg.action === "sat_discover") {
// Browser triggert einen Geraete-Scan auf einem Satelliten.
sendToRVS_raw({ type: "sat_discover",