diff --git a/diagnostic/index.html b/diagnostic/index.html
index 32196f0..4360bd5 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -207,6 +207,9 @@
Main
+ Gehirn
+ Skills
+ Dateien
Einstellungen
@@ -216,16 +219,13 @@
-
OpenClaw Gateway
+
ARIA Brain
-
-
-
Reconnect
-
Agent-Auth pruefen
-
Core Terminal
+
+
Status pruefen
@@ -289,9 +289,6 @@
💭 ARIA denkt...
- 🔧 Reparieren
- 🧹 Compact
- 🚨 Hart neu
Abbrechen
@@ -326,48 +323,8 @@
-
-
-
-
-
Sessions
-
- + Neu
- Laden
-
-
-
- Aktiv:
-
-
-
-
-
-
-
Brain / Memory
- Laden
-
-
-
-
-
π§
-
Gehirn ist leer
-
ARIA speichert Erinnerungen wenn sie etwas Wichtiges lernt
-
-
-
+
@@ -445,6 +402,43 @@
+
+
+
Reparatur & Restart
+
+
+ Wenn ein Container haengt oder ARIA nicht mehr antwortet β hier kann man gezielt eingreifen.
+
+
+ π¨ aria-bridge neu
+ π¨ aria-brain neu
+ π¨ aria-qdrant neu
+
+
+
+
+
+
+
+
Komplett-Reset
+
+
+ β WIPE ALL β alle Einstellungen, Stimmen und das komplette
+ GedΓ€chtnis (Vector-DB) werden gelΓΆscht. ARIA wird leer wie nach
+ Erstinstallation. Bleiben tun nur:
+
+
+ .env (Tokens + RVS-Config)
+ aria-data/ssh/ (SSH-Keys fΓΌr aria-wohnung)
+
+
+ Vorher exportieren wenn etwas erhalten bleiben soll (Gehirn-Tab + Voice-Liste).
+
+
π ALLES lΓΆschen β neue ARIA
+
+
+
+
Betriebsmodus
@@ -472,7 +466,14 @@
-
Sprachausgabe
+
+
Sprachausgabe
+
+ β¬ Export
+
+ β¬ Import
+
+
-
- Compact nach Messages:
-
-
Speichern
Neu laden
@@ -700,16 +697,210 @@
-
+
+
+
+
+
+
+
-
OpenClaw Config
+
Gehirn β Status
-
Config laden
-
(Noch nicht geladen)
+
(Lade...)
+
+
+ Aktualisieren
+ β Jetzt destillieren
+ π§Ή Konversation leeren
+
-
+
+
Bootstrap & Migration
+
+
+ Drei Wege ARIA mit "Grundregeln" zu fΓΌttern β von leichtgewichtig bis Voll-Wiederherstellung.
+
+
+
+
+
1. Aus brain-import/ migrieren
+
+ Parst AGENT.md/USER.md/TOOLING.md aus dem Repo und schreibt sie
+ als atomare pinned Memory-Punkte (identity / rule / preference / tool / skill). Idempotent β Re-Run
+ ersetzt nur die Migration-Punkte, eigene Memories bleiben.
+
+
(Brain-Status laden fΓΌr Datei-Liste)
+
βΆ Migration starten
+
+
+
+
+
2. Bootstrap-Snapshot (nur pinned)
+
+ Klein und schnell: nur die pinned Memories (IdentitΓ€t, Regeln, PrΓ€ferenzen, Tools, Skills) als JSON.
+ Use-Case: Wipe β Bootstrap-Import β ARIA hat PersΓΆnlichkeit zurΓΌck, sonst leer.
+ Cold Memory (Konversations-Fakten) bleibt beim Import unangetastet.
+
+
β¬ Bootstrap exportieren (JSON)
+
+
β¬ Bootstrap importieren
+
+
+
+
+
+
3. Komplettes Gehirn (alles)
+
+ Tar.gz mit Memories + Skills + Qdrant-DB. Brain + Qdrant werden kurz angehalten.
+ Import ΓΌberschreibt ALLES β vorher exportieren wenn etwas erhalten bleiben soll.
+
+
β¬ Gehirn exportieren (tar.gz)
+
+
β¬ Gehirn importieren
+
+
+
+
+
+
+
+
+
+
+
Memories
+
+ Aktualisieren
+ + Neu
+
+
+
+
+
+ Suchen
+
+ Alle Typen
+ IdentitΓ€t
+ Regeln / Werte
+ Praeferenzen
+ Tools
+ Skills
+ Fakten
+ Konversation
+ Reminder
+
+
+ Pinned + Cold
+ π Nur Pinned
+ Nur Cold
+
+ β
+
+
+
+
+
(Brain-Container nicht erreichbar oder leer)
+
+
+
+
+
+
+
+
+
+
Dateien
+ Aktualisieren
+
+
+
+
+
+ Alle
+ Von ARIA (aria_*)
+ Vom Benutzer
+
+
+
+
+
+
+
+
+
+
+
+
+
Skills
+
+ Aktualisieren
+
+ β¬ Import
+
+
+
+
+ Skills sind ARIAs Faehigkeiten. ARIA legt sie selbst an wenn sie ein wiederkehrendes
+ Problem geloest hat. Du kannst sie hier verwalten β ansehen, ausfuehren, deaktivieren,
+ exportieren, importieren. Ein deaktivierter Skill bleibt ARIA bekannt (sie baut ihn
+ nicht doppelt), kann aber nicht aufgerufen werden.
+
+
+
+
+
+
+
+
+
+
+
+
+
Typ
+
+ identity β Wer ARIA ist
+ rule β Sicherheit / Werte / Normen
+ preference β Benutzer-Praeferenzen
+ tool β Tool-Freigaben
+ skill β Faehigkeit / Workflow
+ fact β Wissens-Fakt
+ conversation β Aus Gespraech destilliert
+ reminder β Termin / Aufgabe
+
+
Titel
+
+
Inhalt
+
+
Kategorie (frei, optional)
+
+
Tags (komma-getrennt)
+
+
+
+ π Pinned (Hot Memory β IMMER im System-Prompt)
+
+
+
+
+
+
@@ -817,7 +1008,6 @@
ws.onopen = () => {
addLog('info', 'browser', 'Verbunden mit Diagnostic Server');
- send({ action: 'get_active_session' });
send({ action: 'load_chat_history' });
};
ws.onclose = () => {
@@ -884,6 +1074,42 @@
loadXTTSVoices(); // Liste neu laden
return;
}
+ if (msg.type === 'xtts_voice_exported') {
+ const p = msg.payload || {};
+ const status = document.getElementById('voice-status');
+ if (!p.ok || !p.data) {
+ if (status) status.textContent = 'β Export fehlgeschlagen: ' + (p.error || 'unbekannt');
+ return;
+ }
+ // base64 β Blob β Download
+ try {
+ const bin = atob(p.data);
+ const bytes = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
+ const blob = new Blob([bytes], { type: 'application/gzip' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = (p.name || 'voice') + '.tar.gz';
+ document.body.appendChild(a); a.click();
+ setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
+ if (status) status.textContent = 'β ' + p.name + ' exportiert (' + (bytes.length/1024).toFixed(0) + ' KB)';
+ } catch (e) {
+ if (status) status.textContent = 'β Decode fehlgeschlagen: ' + e.message;
+ }
+ return;
+ }
+ if (msg.type === 'xtts_voice_imported') {
+ const p = msg.payload || {};
+ const status = document.getElementById('voice-status');
+ if (p.ok) {
+ if (status) status.textContent = 'β Stimme "' + p.name + '" importiert';
+ loadXTTSVoices();
+ } else {
+ if (status) status.textContent = 'β Import fehlgeschlagen: ' + (p.error || 'unbekannt');
+ }
+ return;
+ }
if (msg.type === 'voice_config') {
document.getElementById('diag-tts-enabled').checked = msg.ttsEnabled !== false;
@@ -974,6 +1200,22 @@
addAriaFile(p);
return;
}
+ if (msg.type === 'file_deleted') {
+ if (msg.path) markFileDeletedInChat(msg.path);
+ // Falls Datei-Manager grade offen ist: Liste refreshen
+ if (typeof loadFiles === 'function' && document.getElementById('tab-files') && document.getElementById('tab-files').classList.contains('visible')) {
+ loadFiles();
+ }
+ return;
+ }
+ if (msg.type === 'skill_created') {
+ addSkillCreatedBubble(msg.payload || {});
+ // Falls Skills-Tab offen: refreshen
+ if (document.getElementById('tab-skills') && document.getElementById('tab-skills').classList.contains('visible')) {
+ loadSkills();
+ }
+ return;
+ }
if (msg.type === 'chat_delta') { return; }
if (msg.type === 'chat_error') {
addChat('error', msg.error, 'chat:error');
@@ -1084,42 +1326,6 @@
return;
}
- // Session + Brain Viewer
- if (msg.type === 'sessions_list') { renderSessions(msg); return; }
- if (msg.type === 'session_detail') { renderSessionDetail(msg); return; }
- if (msg.type === 'session_deleted') {
- if (msg.ok) loadSessions();
- else alert('Loeschen fehlgeschlagen: ' + (msg.error || '?'));
- return;
- }
- if (msg.type === 'session_export') {
- if (!msg.ok) { alert('Export fehlgeschlagen: ' + (msg.error || '?')); return; }
- const blob = new Blob([msg.markdown], { type: 'text/markdown;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = msg.filename;
- document.body.appendChild(a);
- a.click();
- setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
- return;
- }
- if (msg.type === 'active_session') {
- updateActiveSessionBar(msg.sessionKey);
- loadSessions(); // Tabelle neu rendern
- send({ action: 'load_chat_history' }); // Chat-Verlauf der neuen Session laden
- return;
- }
- if (msg.type === 'session_created') {
- if (msg.ok) {
- loadSessions();
- } else {
- alert('Session erstellen fehlgeschlagen: ' + (msg.error || '?'));
- }
- return;
- }
- if (msg.type === 'brain_list') { renderBrainList(msg); return; }
- if (msg.type === 'brain_content') { renderBrainContent(msg); return; }
// Settings (permissions_list/permissions_saved entfernt β Alles-oder-Nichts via --dangerously-skip-permissions)
if (msg.type === 'session_restarted') {
const s = document.getElementById('perms-status');
@@ -1481,30 +1687,92 @@
const url = serverPath; // Diagnostic-Server liefert /shared/* aus
const sizeStr = sizeKB > 1024 ? `${(sizeKB/1024).toFixed(1)}MB` : `${sizeKB}KB`;
const icon = isImage ? 'πΌοΈ' : isPdf ? 'π' : 'π';
- // PDFs/Bilder: target=_blank β neuer Tab. Andere: download-Attribut.
- const linkAttrs = (isImage || isPdf)
- ? `href="${url}" target="_blank" rel="noopener"`
- : `href="${url}" download="${escapeHtml(name)}"`;
- let preview = '';
- if (isImage) {
- preview = `
`;
+ const deleted = !!p.deleted;
+ let html;
+ if (deleted) {
+ html = `
${icon} ${escapeHtml(name)} β vom Benutzer gelΓΆscht
` +
+ `
(${escapeHtml(mimeType)}) ` +
+ `
${escapeHtml(serverPath)}
` +
+ `
ARIA-Datei (gelΓΆscht)
`;
+ } else {
+ const linkAttrs = (isImage || isPdf)
+ ? `href="${url}" target="_blank" rel="noopener"`
+ : `href="${url}" download="${escapeHtml(name)}"`;
+ let preview = '';
+ if (isImage) {
+ preview = `
`;
+ }
+ html = `
${icon} ARIA hat eine Datei erstellt
` +
+ `
${escapeHtml(name)} ` +
+ `
(${escapeHtml(mimeType)}, ${sizeStr}) ` +
+ preview +
+ `
${escapeHtml(serverPath)}
` +
+ `
ARIA-Datei β ${new Date().toLocaleTimeString('de-DE')}
`;
}
- const html = `
${icon} ARIA hat eine Datei erstellt
` +
- `
${escapeHtml(name)} ` +
- `
(${escapeHtml(mimeType)}, ${sizeStr}) ` +
- preview +
- `
${escapeHtml(serverPath)}
` +
- `
ARIA-Datei β ${new Date().toLocaleTimeString('de-DE')}
`;
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const el = document.createElement('div');
el.className = 'chat-msg received';
+ el.dataset.ariaFilePath = serverPath;
+ if (deleted) el.dataset.deleted = '1';
el.innerHTML = html;
box.appendChild(el);
box.scrollTop = box.scrollHeight;
}
}
+ /** ARIA hat einen Skill erstellt β als auffaellige Bubble anzeigen. */
+ function addSkillCreatedBubble(skill) {
+ const name = skill.name || '(unbenannt)';
+ const desc = skill.description || '(ohne Beschreibung)';
+ const execMode = skill.execution || 'bash';
+ const active = skill.active !== false;
+ const setupErr = skill.setup_error
+ ? `
β Setup-Fehler: ${escapeHtml(skill.setup_error.slice(0,200))}
`
+ : '';
+ const statusBadge = active
+ ? '
aktiv '
+ : '
deaktiviert ';
+ const html = `
+
π ARIA hat einen neuen Skill erstellt
+
+ ${escapeHtml(name)}
+ (${escapeHtml(execMode)}, ${statusBadge})
+
+
${escapeHtml(desc)}
+ ${setupErr}
+
`;
+ for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
+ if (!box) continue;
+ const el = document.createElement('div');
+ el.className = 'chat-msg received';
+ el.style.borderLeft = '3px solid #FFD60A';
+ el.innerHTML = html;
+ box.appendChild(el);
+ box.scrollTop = box.scrollHeight;
+ }
+ }
+
+ /** Wenn der Server file_deleted broadcastet: alle Bubbles mit
+ diesem serverPath rerendern als "geloescht" markieren. */
+ function markFileDeletedInChat(serverPath) {
+ for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
+ if (!box) continue;
+ const bubbles = box.querySelectorAll(`[data-aria-file-path="${CSS.escape(serverPath)}"]`);
+ bubbles.forEach(el => {
+ if (el.dataset.deleted === '1') return;
+ el.dataset.deleted = '1';
+ const name = (serverPath.split('/').pop()) || 'datei';
+ el.innerHTML = `
π ${escapeHtml(name)} β vom Benutzer gelΓΆscht
` +
+ `
${escapeHtml(serverPath)}
` +
+ `
ARIA-Datei (gelΓΆscht)
`;
+ });
+ }
+ }
+
let chatFullscreen = false;
function toggleChatFullscreen() {
const modal = document.getElementById('chat-fullscreen');
@@ -1670,13 +1938,55 @@
+ `
${esc(v.name)} `
+ `
${(v.size/1024).toFixed(0)}KB `
+ `
βΆ `
+ + `
β¬ `
+ `
X `
+ `
`;
}
html += '';
+ html += '';
+ html += ' ';
+ html += 'β¬ Stimme importieren ';
+ html += '
';
box.innerHTML = html;
}
+ // ββ Voice Export/Import ββββββββββββββββββββββββββββ
+ function exportXttsVoice(name) {
+ const status = document.getElementById('voice-status');
+ if (status) status.textContent = 'β³ Exportiere ' + name + ' ...';
+ // RVS request/response β Antwort kommt via 'xtts_voice_exported'
+ send({ action: 'xtts_export_voice', name });
+ }
+
+ async function importXttsVoice(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+ const status = document.getElementById('voice-status');
+ try {
+ // Datei als base64 lesen (kann mehrere MB sein)
+ const buf = await file.arrayBuffer();
+ const bytes = new Uint8Array(buf);
+ let bin = '';
+ const chunk = 8192;
+ for (let i = 0; i < bytes.length; i += chunk) {
+ bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
+ }
+ const b64 = btoa(bin);
+ // Name aus Dateinamen ableiten β z.B. "maia.tar.gz" β "maia"
+ const baseName = file.name.replace(/\.tar\.gz$|\.tgz$/i, '');
+ if (!confirm(`Stimme "${baseName}" importieren?\n\nFalls schon vorhanden, wird sie ΓΌberschrieben.`)) {
+ event.target.value = '';
+ return;
+ }
+ if (status) status.textContent = 'β³ Lade hoch (' + (file.size/1024).toFixed(0) + ' KB)...';
+ send({ action: 'xtts_import_voice', name: baseName, data: b64 });
+ } catch (e) {
+ if (status) status.textContent = 'β ' + e.message;
+ } finally {
+ event.target.value = '';
+ }
+ }
+
// ββ Voice Preview Modal βββββββββββββββββββββββββ
const VOICE_PREVIEW_DEFAULT = 'Hallo, ich bin ARIA. Das hier ist ein kleiner Test damit du meine Stimme beurteilen kannst.';
const PREVIEW_SPEED_DEFAULT = 1.0;
@@ -1843,47 +2153,90 @@
renderDiagPending();
}
- // ββ Reparieren β openclaw doctor --fix ββββββ
- function doctorFix() {
- fetch('/api/doctor-fix', { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- if (data.ok) {
- addLog('info', 'server', 'Reparatur ausgefuehrt: ' + (data.output || 'OK').slice(0, 200));
- } else {
- addLog('error', 'server', 'Reparatur fehlgeschlagen: ' + (data.error || ''));
- }
- })
- .catch(err => addLog('error', 'server', 'Reparatur Request fehlgeschlagen: ' + err.message));
+ // doctorFix + ariaRestart entfernt β aria-core ist raus.
+ // Fuer Container-Restarts: restartContainer('aria-bridge'|'aria-brain'|'aria-qdrant').
+
+ // ββ Voice Settings Export/Import ββββββββββββββββββββββ
+ function exportVoiceSettings() {
+ window.location.href = '/api/voice-config-export';
}
- // ββ Hard-Restart β docker restart aria-core ββββββ
- function ariaRestart() {
- if (!confirm('ARIA wird hart neu gestartet (Container-Restart, ~15s).\n\nLaufende Anfragen gehen verloren. Sicher?')) return;
- fetch('/api/aria-restart', { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- if (data.ok) {
- addLog('info', 'server', 'ARIA neu gestartet β wartet auf Reconnect');
- } else {
- addLog('error', 'server', 'Restart fehlgeschlagen: ' + (data.error || ''));
- }
- })
- .catch(err => addLog('error', 'server', 'Restart Request fehlgeschlagen: ' + err.message));
+ async function importVoiceSettings(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+ try {
+ const text = await file.text();
+ // Sanity-Check: muss JSON sein
+ JSON.parse(text);
+ if (!confirm(`Voice-Settings aus "${file.name}" importieren?\n\nAktuelle voice_config + highlight_triggers werden ΓΌberschrieben.`)) {
+ event.target.value = '';
+ return;
+ }
+ const r = await fetch('/api/voice-config-import', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: text,
+ });
+ const d = await r.json();
+ if (d.ok) {
+ alert('β ' + d.message);
+ send({ action: 'get_voice_config' });
+ } else {
+ alert('Import fehlgeschlagen: ' + d.error);
+ }
+ } catch (e) {
+ alert('UngΓΌltige Datei: ' + e.message);
+ } finally {
+ event.target.value = '';
+ }
}
- // ββ Compact / Session-Reset ββββββ
- function ariaSessionReset() {
- if (!confirm('Konversation komprimieren: alle Nachrichten in ARIAs aktueller Session werden geloescht und der Container neu gestartet. ARIA vergisst den bisherigen Gespraechsverlauf. Sicher?')) return;
- fetch('/api/aria-session-reset', { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- if (data.ok) addLog('info', 'server', 'Session geleert, ARIA neu gestartet');
- else addLog('error', 'server', 'Reset fehlgeschlagen: ' + (data.error || ''));
- })
- .catch(err => addLog('error', 'server', 'Reset Request fehlgeschlagen: ' + err.message));
+ // ββ Wipe All β Komplett-Reset ββββββββββββββββββββββββββ
+ async function wipeAll() {
+ const statusEl = document.getElementById('wipe-status');
+ if (!confirm('WIRKLICH alles lΓΆschen?\n\nGedΓ€chtnis + Stimmen + Settings β komplett weg.\n.env + SSH-Keys bleiben.\n\nVorher exportieren wenn was erhalten bleiben soll.')) return;
+ if (!confirm('Letzte Warnung: Das KANN NICHT rΓΌckgΓ€ngig gemacht werden.\n\nFortfahren?')) return;
+ statusEl.innerHTML = 'β³ Stoppe Container, lΓΆsche Daten...';
+ try {
+ const r = await fetch('/api/wipe-all', { method: 'POST' });
+ const d = await r.json();
+ if (d.ok) {
+ statusEl.innerHTML = `β ${d.message} `;
+ setTimeout(() => location.reload(), 3000);
+ } else {
+ statusEl.innerHTML = `β ${d.error} `;
+ }
+ } catch (e) {
+ statusEl.innerHTML = `β ${e.message} `;
+ }
}
+ // ββ Generischer Container-Restart (Bridge/Brain/Qdrant) ββββββ
+ async function restartContainer(name) {
+ const statusEl = document.getElementById('restart-status');
+ if (!confirm(`Container "${name}" wirklich neu starten?\n\nLaufende Anfragen gehen verloren.`)) return;
+ if (statusEl) statusEl.innerHTML = `β³ Restart ${name}...`;
+ try {
+ const r = await fetch('/api/container-restart', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name }),
+ });
+ const d = await r.json();
+ if (d.ok) {
+ if (statusEl) statusEl.innerHTML = `β ${name} neu gestartet `;
+ addLog('info', 'server', `${name} neu gestartet`);
+ } else {
+ if (statusEl) statusEl.innerHTML = `β ${d.error} `;
+ addLog('error', 'server', `${name} Restart: ${d.error}`);
+ }
+ } catch (e) {
+ if (statusEl) statusEl.innerHTML = `β ${e.message} `;
+ }
+ }
+
+ // ariaSessionReset entfernt β aria-core ist raus.
+
// ββ Abbrechen ββββββββββββββββββββββββββββββ
function cancelRequest() {
send({ action: 'cancel_request' });
@@ -2256,233 +2609,8 @@
}
}
- // ββ Session Viewer ββββββββββββββββββββββββββββββββββββββββ
-
- function loadSessions() {
- document.getElementById('sessions-list').innerHTML = 'Lade...
';
- send({ action: 'list_sessions' });
- }
-
- let currentActiveSession = '';
-
- function renderSessions(data) {
- const container = document.getElementById('sessions-list');
- if (data.error) {
- container.innerHTML = `Fehler: ${escapeHtml(data.error)}
`;
- return;
- }
- if (!data.sessions || data.sessions.length === 0) {
- container.innerHTML = data.raw
- ? `${escapeHtml(data.raw)} `
- : 'Keine Sessions gefunden
';
- return;
- }
-
- const active = data.sessions.filter(s => !s.archived);
- const archives = data.sessions.filter(s => s.archived);
-
- const headerRow = ''
- + 'Session '
- + 'Msgs '
- + 'Zuletzt '
- + ' ';
-
- const rowFor = (s, opts) => {
- const date = s.modified ? new Date(s.modified * 1000).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : '?';
- const key = escapeHtml(s.sessionKey || s.path.split('/').pop());
- const orphanBadge = s.orphan ? ' verwaist ' : '';
- const archivedBadge = s.archived ? ' archiv ' : '';
- const modelBadge = s.model ? `${escapeHtml(s.model)}
` : '';
- const isActive = (s.sessionKey === currentActiveSession) && !s.archived;
- const keyColor = isActive ? '#34C759' : (s.archived || s.orphan ? '#8888AA' : '#E0E0F0');
- const activeBadge = isActive ? ' aktiv ' : '';
- const rowBg = isActive ? 'background:rgba(52,199,89,0.08);' : (s.archived ? 'background:rgba(136,136,170,0.04);' : '');
-
- let actions = '';
- if (s.archived) {
- // Archive: nur Export + Loeschen (kein Aktivieren β wuerde aktive Session ueberschreiben)
- actions = `X `
- + `⬇ `;
- } else {
- actions = (isActive ? '' : `▶ `)
- + `X `
- + `⬇ `;
- }
-
- return ``
- + ``
- + `${key}${activeBadge}${orphanBadge}${archivedBadge}
${modelBadge} `
- + `${s.lines} `
- + `${date} `
- + `${actions} `;
- };
-
- let html = '' + headerRow;
- for (const s of active) html += rowFor(s);
- html += '
';
-
- if (archives.length > 0) {
- html += ``
- + ``
- + `Archivierte Versionen (${archives.length}) β von OpenClaw beim Session-Reset gesichert`
- + ` `
- + `` + headerRow;
- for (const s of archives) html += rowFor(s);
- html += '
';
- }
-
- container.innerHTML = html;
- }
-
- function viewSession(path) {
- const detail = document.getElementById('session-detail');
- const title = document.getElementById('session-detail-title');
- const content = document.getElementById('session-detail-content');
- detail.style.display = 'block';
- title.textContent = path.split('/').pop();
- content.innerHTML = 'Lade...
';
- send({ action: 'read_session', sessionPath: path });
- }
-
- function renderSessionDetail(data) {
- const content = document.getElementById('session-detail-content');
- if (data.error) {
- content.innerHTML = `${escapeHtml(data.error)}
`;
- return;
- }
- if (data.raw) {
- content.innerHTML = `${escapeHtml(data.raw)} `;
- return;
- }
- if (!data.messages || data.messages.length === 0) {
- content.innerHTML = 'Keine Nachrichten
';
- return;
- }
- let html = '';
- for (const msg of data.messages) {
- const role = msg.role || msg.type || '?';
- const text = extractMessageText(msg);
- const roleColor = role === 'user' ? '#0096FF' : role === 'assistant' ? '#34C759' : '#8888AA';
- html += ``
- + `${escapeHtml(role)} `
- + `${escapeHtml(text.slice(0, 500))}${text.length > 500 ? '...' : ''} `
- + '
';
- }
- content.innerHTML = html;
- }
-
- function extractMessageText(msg) {
- if (typeof msg.content === 'string') return msg.content;
- if (Array.isArray(msg.content)) {
- return msg.content.filter(c => c.type === 'text').map(c => c.text || '').join('');
- }
- if (msg.text) return msg.text;
- if (msg.message) return typeof msg.message === 'string' ? msg.message : JSON.stringify(msg.message);
- return JSON.stringify(msg).slice(0, 200);
- }
-
- function closeSessionDetail() {
- document.getElementById('session-detail').style.display = 'none';
- }
-
- function deleteSession(path) {
- const name = path.split('/').pop();
- if (!confirm(`Session "${name}" wirklich loeschen?`)) return;
- send({ action: 'delete_session', sessionPath: path });
- }
-
- function exportSession(path, sessionKey) {
- send({ action: 'export_session', sessionPath: path, sessionKey });
- }
-
- function activateSession(sessionKey) {
- send({ action: 'set_active_session', sessionKey });
- }
-
- function createSession() {
- const name = prompt('Name fuer neue Session (a-z, 0-9, -, _):');
- if (!name) return;
- send({ action: 'create_session', sessionName: name.trim() });
- }
-
- function updateActiveSessionBar(sessionKey) {
- currentActiveSession = sessionKey || '';
- const bar = document.getElementById('active-session-bar');
- const nameEl = document.getElementById('active-session-name');
- if (currentActiveSession) {
- bar.style.display = 'block';
- nameEl.textContent = currentActiveSession;
- } else {
- bar.style.display = 'none';
- }
- }
-
- // ββ Brain Viewer ββββββββββββββββββββββββββββββββββββββββ
-
- function loadBrain() {
- document.getElementById('brain-list').innerHTML = 'Lade...
';
- document.getElementById('brain-empty').style.display = 'none';
- send({ action: 'list_brain' });
- }
-
- function renderBrainList(data) {
- const container = document.getElementById('brain-list');
- const emptyEl = document.getElementById('brain-empty');
-
- if (data.error) {
- container.innerHTML = `Fehler: ${escapeHtml(data.error)}
`;
- emptyEl.style.display = 'none';
- return;
- }
- if (!data.files || data.files.length === 0) {
- container.innerHTML = '';
- emptyEl.style.display = 'block';
- return;
- }
- emptyEl.style.display = 'none';
-
- const TYPE_COLORS = { user: '#0096FF', feedback: '#FFD60A', project: '#34C759', reference: '#FF9500' };
- let html = '';
- for (const f of data.files) {
- if (f.name === '.gitkeep') continue;
- const color = TYPE_COLORS[f.memType] || '#8888AA';
- const date = f.modified ? new Date(f.modified * 1000).toLocaleString('de-DE') : '?';
- html += ``
- + `
`
- + `
`
- + `
${escapeHtml(f.name)}
`
- + (f.description ? `
${escapeHtml(f.description)}
` : '')
- + `
`
- + `
${escapeHtml(f.size)}
`
- + '
';
- }
- container.innerHTML = html || 'Nur .gitkeep gefunden
';
- }
-
- function viewBrainFile(name) {
- const panel = document.getElementById('brain-content');
- const title = document.getElementById('brain-content-title');
- const text = document.getElementById('brain-content-text');
- panel.style.display = 'block';
- title.textContent = name;
- text.textContent = 'Lade...';
- send({ action: 'read_brain_file', filename: name });
- }
-
- function renderBrainContent(data) {
- const text = document.getElementById('brain-content-text');
- if (data.error) {
- text.textContent = `Fehler: ${data.error}`;
- text.style.color = '#FF6B6B';
- return;
- }
- text.style.color = '#E0E0F0';
- text.textContent = data.content || '(leer)';
- }
-
- function closeBrainContent() {
- document.getElementById('brain-content').style.display = 'none';
- }
+ // Sessions- und alter Brain-Viewer wurden entfernt β Memories laufen
+ // jetzt komplett ueber den Gehirn-Tab + Vector-DB im aria-brain.
// ββ Haupt-Tab Navigation ββββββββββββββββββββββββββββββββββ
@@ -2491,15 +2619,691 @@
document.querySelectorAll('.main-nav-btn').forEach(b => b.classList.remove('active'));
const target = document.getElementById('tab-' + tab);
if (target) target.classList.add('visible');
- // Button aktivieren
+ // Button aktivieren β Match per onclick-Attribut (robust gegen Beschriftungs-Aenderungen)
document.querySelectorAll('.main-nav-btn').forEach(b => {
- if (b.textContent.trim().toLowerCase().includes(tab === 'main' ? 'main' : 'einstellung')) b.classList.add('active');
+ const oc = b.getAttribute('onclick') || '';
+ if (oc.includes(`'${tab}'`)) b.classList.add('active');
});
// Einstellungen: Config + QR laden
if (tab === 'settings') {
send({ action: 'get_voice_config' });
loadRuntimeConfig();
loadOnboardingQR();
+ } else if (tab === 'brain') {
+ loadBrainStatus();
+ loadBrainMemoryList();
+ refreshImportFiles();
+ } else if (tab === 'files') {
+ loadFiles();
+ } else if (tab === 'skills') {
+ loadSkills();
+ }
+ }
+
+ // ββ Skills-Verwaltung ββββββββββββββββββββββββββββ
+ let skillsCache = [];
+ const skillExpanded = new Set();
+
+ async function loadSkills() {
+ const el = document.getElementById('skills-list');
+ if (!el) return;
+ try {
+ const r = await fetch('/api/brain/skills/list');
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const d = await r.json();
+ skillsCache = d.skills || [];
+ renderSkillsList();
+ } catch (e) {
+ el.innerHTML = `π΄ Brain nicht erreichbar (${e.message})`;
+ }
+ }
+
+ function renderSkillsList() {
+ const el = document.getElementById('skills-list');
+ if (!el) return;
+ if (!skillsCache.length) {
+ el.innerHTML = 'Keine Skills vorhanden. ARIA legt welche an wenn sie ein wiederkehrendes Problem lΓΆst β oder importiere einen.
';
+ return;
+ }
+ const fmtDate = (iso) => iso ? new Date(iso).toLocaleString('de-DE') : 'β';
+ el.innerHTML = skillsCache.map(s => {
+ const active = s.active !== false;
+ const expanded = skillExpanded.has(s.name);
+ const statusBadge = active
+ ? 'aktiv '
+ : 'DEAKTIVIERT ';
+ const execBadge = `${escapeHtml(s.execution || 'bash')} `;
+ const authorBadge = s.author === 'aria'
+ ? 'von ARIA '
+ : '';
+ const setupErr = s.setup_error
+ ? `β Setup-Fehler: ${escapeHtml(s.setup_error.slice(0,200))}
`
+ : '';
+ let expandedSection = '';
+ if (expanded) {
+ expandedSection = `
+
+
(README lΓ€dt...)
+
+ βΆ AusfΓΌhren
+ ${active ? 'βΈ Deaktivieren' : 'βΆ Aktivieren'}
+ β¬ Export
+ π LΓΆschen
+
+
Logs (letzte 20)
+
(Logs lΓ€dt...)
+
+ `;
+ }
+ return `
+
+
+ ${expanded ? 'βΌ' : 'βΆ'}
+ ${escapeHtml(s.name)}
+ ${statusBadge} ${execBadge} ${authorBadge}
+ ${s.use_count || 0}Γ Β· zuletzt ${fmtDate(s.last_used)}
+
+
${escapeHtml(s.description || '(ohne Beschreibung)')}
+ ${setupErr}
+ ${expandedSection}
+
+ `;
+ }).join('');
+ }
+
+ async function toggleSkillExpand(name) {
+ if (skillExpanded.has(name)) skillExpanded.delete(name);
+ else skillExpanded.add(name);
+ renderSkillsList();
+ if (skillExpanded.has(name)) {
+ // README + Logs lazy laden
+ try {
+ const r = await fetch('/api/brain/skills/' + encodeURIComponent(name));
+ const d = await r.json();
+ const el = document.getElementById('skill-readme-' + name);
+ if (el && d.readme) el.innerHTML = '' + escapeHtml(d.readme) + ' ';
+ } catch {}
+ try {
+ const r2 = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/logs');
+ const d2 = await r2.json();
+ const el2 = document.getElementById('skill-logs-' + name);
+ if (!el2) return;
+ if (!d2.logs.length) { el2.innerHTML = '(keine Runs)'; return; }
+ el2.innerHTML = d2.logs.map(l => {
+ const okBadge = l.exit_code === 0 ? 'OK ' : `FEHLER (${l.exit_code}) `;
+ return `
+
${escapeHtml(l.ts)} Β· ${l.duration_sec}s Β· ${okBadge}
+ ${l.stdout ? `
${escapeHtml(l.stdout)} ` : ''}
+ ${l.stderr ? `
${escapeHtml(l.stderr)} ` : ''}
+
`;
+ }).join('');
+ } catch {}
+ }
+ }
+
+ async function toggleSkillActive(name, newActive) {
+ try {
+ await fetch('/api/brain/skills/' + encodeURIComponent(name), {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ active: newActive }),
+ });
+ loadSkills();
+ } catch (e) {
+ alert('Toggle fehlgeschlagen: ' + e.message);
+ }
+ }
+
+ async function deleteSkill(name) {
+ if (!confirm(`Skill "${name}" wirklich lΓΆschen?\n\nFolder, venv, Logs β alles weg.`)) return;
+ try {
+ await fetch('/api/brain/skills/' + encodeURIComponent(name), { method: 'DELETE' });
+ skillExpanded.delete(name);
+ loadSkills();
+ } catch (e) {
+ alert('LΓΆschen fehlgeschlagen: ' + e.message);
+ }
+ }
+
+ async function runSkillPrompt(name) {
+ const argsStr = prompt(`Skill "${name}" ausfΓΌhren.\n\nArgs als JSON (oder leer):`, '{}');
+ if (argsStr === null) return;
+ let args = {};
+ try {
+ args = JSON.parse(argsStr || '{}');
+ } catch (e) {
+ alert('UngΓΌltiges JSON: ' + e.message);
+ return;
+ }
+ try {
+ const r = await fetch('/api/brain/skills/run', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, args }),
+ });
+ const d = await r.json();
+ const summary = `${d.ok ? 'β OK' : 'β FEHLER'} Β· ${d.duration_sec}s Β· exit=${d.exit_code}\n\nstdout:\n${(d.stdout||'').slice(0,1500)}\n\nstderr:\n${(d.stderr||'').slice(0,500)}`;
+ alert(summary);
+ if (skillExpanded.has(name)) toggleSkillExpand(name); // refresh logs by collapse+expand
+ loadSkills();
+ } catch (e) {
+ alert('Run fehlgeschlagen: ' + e.message);
+ }
+ }
+
+ function exportSkill(name) {
+ window.location.href = '/api/brain/skills/' + encodeURIComponent(name) + '/export';
+ }
+
+ async function importSkillFile(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+ if (!confirm(`Skill aus "${file.name}" importieren?\n\nFalls schon vorhanden mit gleichem Namen, wird er ΓΌberschrieben.`)) {
+ event.target.value = ''; return;
+ }
+ try {
+ const r = await fetch('/api/brain/skills/import?overwrite=true', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/gzip' },
+ body: file,
+ });
+ const d = await r.json();
+ if (r.ok) {
+ alert(`β Skill "${d.imported?.name}" importiert.`);
+ loadSkills();
+ } else {
+ alert('Import fehlgeschlagen: ' + (d.detail || JSON.stringify(d)));
+ }
+ } catch (e) {
+ alert('Import fehlgeschlagen: ' + e.message);
+ } finally {
+ event.target.value = '';
+ }
+ }
+
+ // ββ Datei-Manager ββββββββββββββββββββββββββββββββββββββ
+ let filesCache = [];
+
+ async function loadFiles() {
+ const listEl = document.getElementById('files-list');
+ if (listEl) listEl.innerHTML = '(Lade...)';
+ try {
+ const r = await fetch('/api/files-list');
+ const d = await r.json();
+ if (!d.ok) throw new Error(d.error || 'Unbekannter Fehler');
+ filesCache = d.files || [];
+ renderFilesList();
+ } catch (e) {
+ if (listEl) listEl.innerHTML = `π΄ ${e.message}`;
+ }
+ }
+
+ function renderFilesList() {
+ const listEl = document.getElementById('files-list');
+ const infoEl = document.getElementById('files-info');
+ if (!listEl) return;
+ const q = (document.getElementById('files-search').value || '').toLowerCase();
+ const filter = document.getElementById('files-filter').value;
+ let files = filesCache.slice();
+ if (filter === 'aria') files = files.filter(f => f.fromAria);
+ else if (filter === 'user') files = files.filter(f => !f.fromAria);
+ if (q) files = files.filter(f => f.name.toLowerCase().includes(q));
+ if (infoEl) infoEl.textContent = `${files.length} von ${filesCache.length} Dateien`;
+ if (!files.length) {
+ listEl.innerHTML = '(Keine Dateien gefunden)';
+ return;
+ }
+ const fmtSize = (b) => b < 1024 ? `${b} B` : b < 1024*1024 ? `${(b/1024).toFixed(1)} KB` : `${(b/1024/1024).toFixed(1)} MB`;
+ const fmtDate = (ms) => new Date(ms).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
+ listEl.innerHTML = files.map(f => {
+ const badge = f.fromAria
+ ? 'ARIA '
+ : 'User ';
+ return `
+
+
${badge}${escapeHtml(f.name)}
+
${fmtSize(f.size)} Β· ${fmtDate(f.mtime)}
+
+
β¬
+
π
+
`;
+ }).join('');
+ }
+
+ function downloadFile(encPath) {
+ window.location.href = '/api/files-download?path=' + encPath;
+ }
+
+ async function deleteFile(p, name) {
+ if (!confirm(`Datei "${name}" wirklich lΓΆschen?\n\nIn allen Chat-Bubbles wird sie als gelΓΆscht markiert.`)) return;
+ try {
+ const r = await fetch('/api/files-delete', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ path: p }),
+ });
+ const d = await r.json();
+ if (d.ok) {
+ loadFiles();
+ // Server broadcastet file_deleted Event β markFileDeletedInChat() updated bubbles
+ } else {
+ alert('LΓΆschen fehlgeschlagen: ' + d.error);
+ }
+ } catch (e) {
+ alert('LΓΆschen fehlgeschlagen: ' + e.message);
+ }
+ }
+
+ // ββ Gehirn-Tab ββββββββββββββββββββββββββββββββββββββββββββ
+
+ async function loadBrainStatus() {
+ const el = document.getElementById('brain-status');
+ if (!el) return;
+ try {
+ const r = await fetch('/api/brain/health');
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const d = await r.json();
+ const st = d.status === 'ok' ? 'π’ online' : 'π‘ ' + (d.status || 'unknown');
+ el.innerHTML = `${st} Β· ${d.memory_count ?? '?'} Memories Β· Qdrant: ${d.qdrant || '-'}`;
+ } catch (e) {
+ el.innerHTML = `π΄ Brain nicht erreichbar (${e.message})`;
+ }
+ // Conversation-Stats (separater Endpoint)
+ const conv = document.getElementById('conversation-status');
+ if (!conv) return;
+ try {
+ const r2 = await fetch('/api/brain/conversation/stats');
+ if (!r2.ok) throw new Error('HTTP ' + r2.status);
+ const d2 = await r2.json();
+ const distillIcon = d2.needs_distill ? ' β Destillat bald fΓ€llig' : '';
+ conv.innerHTML = `Konversation: ${d2.turns} Turns Β· Window: ${d2.max_window} Β· Schwelle: ${d2.distill_threshold}${distillIcon}`;
+ } catch (e) {
+ conv.innerHTML = `Konversation: ${e.message} `;
+ }
+ }
+
+ async function distillNow() {
+ if (!confirm('Destillat manuell auslΓΆsen?\n\nDie Γ€ltesten Turns werden zu fact-Memories verdichtet β kostet einen Claude-Call.')) return;
+ try {
+ const r = await fetch('/api/brain/conversation/distill', { method: 'POST' });
+ const d = await r.json();
+ alert(`Destillat: ${d.distilled || 0} Facts geschrieben, ${d.removed_turns || 0} Turns entfernt.${d.error ? '\nFehler: ' + d.error : ''}`);
+ loadBrainStatus();
+ loadBrainMemoryList();
+ } catch (e) {
+ alert('Destillat fehlgeschlagen: ' + e.message);
+ }
+ }
+
+ async function resetConversation() {
+ if (!confirm('Konversation leeren?\n\nDer Rolling-Window-Verlauf wird komplett verworfen. Destillierte Facts bleiben in der DB.')) return;
+ try {
+ const r = await fetch('/api/brain/conversation/reset', { method: 'POST' });
+ const d = await r.json();
+ if (d.ok) {
+ loadBrainStatus();
+ } else {
+ alert('Reset fehlgeschlagen');
+ }
+ } catch (e) {
+ alert('Reset fehlgeschlagen: ' + e.message);
+ }
+ }
+
+ // Cache aller geladenen Memories β fuer Edit-Lookup
+ let brainMemoryCache = {};
+ // Aktuelle Search-Treffer (IDs in Reihenfolge); leer = normale Liste
+ let brainSearchIds = null;
+
+ function resetBrainFilters() {
+ const s = document.getElementById('brain-search'); if (s) s.value = '';
+ const t = document.getElementById('brain-filter-type'); if (t) t.value = '';
+ const p = document.getElementById('brain-filter-pinned'); if (p) p.value = 'all';
+ const info = document.getElementById('brain-search-info'); if (info) info.style.display = 'none';
+ brainSearchIds = null;
+ }
+
+ async function runBrainSearch() {
+ const q = (document.getElementById('brain-search').value || '').trim();
+ const info = document.getElementById('brain-search-info');
+ if (!q) {
+ brainSearchIds = null;
+ if (info) info.style.display = 'none';
+ loadBrainMemoryList();
+ return;
+ }
+ const typeFilter = document.getElementById('brain-filter-type').value;
+ const params = new URLSearchParams({ q, k: '20', include_pinned: 'true' });
+ if (typeFilter) params.set('type', typeFilter);
+ try {
+ const r = await fetch('/api/brain/memory/search?' + params.toString());
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const hits = await r.json();
+ hits.forEach(m => { brainMemoryCache[m.id] = m; });
+ brainSearchIds = hits.map(m => m.id);
+ if (info) {
+ info.style.display = 'block';
+ info.innerHTML = `π ${hits.length} Treffer fΓΌr "${escapeHtml(q)}"` +
+ (typeFilter ? ` Β· Typ=${escapeHtml(typeFilter)}` : '') +
+ ` Β· sortiert nach Aehnlichkeit`;
+ }
+ renderBrainList(hits, true);
+ } catch (e) {
+ if (info) {
+ info.style.display = 'block';
+ info.innerHTML = `π΄ Suche fehlgeschlagen: ${escapeHtml(e.message)}`;
+ }
+ }
+ }
+
+ async function loadBrainMemoryList() {
+ const el = document.getElementById('brain-memory-list');
+ if (!el) return;
+ // Wenn aktive Search-Treffer da sind: die anzeigen, nicht neu laden
+ if (brainSearchIds && brainSearchIds.length) {
+ const items = brainSearchIds.map(id => brainMemoryCache[id]).filter(Boolean);
+ renderBrainList(items, true);
+ return;
+ }
+ try {
+ const typeFilter = document.getElementById('brain-filter-type').value;
+ const pinnedFilter = document.getElementById('brain-filter-pinned').value;
+ const params = new URLSearchParams({ limit: '500' });
+ if (typeFilter) params.set('type', typeFilter);
+ const r = await fetch('/api/brain/memory/list?' + params.toString());
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ let items = await r.json();
+ brainMemoryCache = {};
+ items.forEach(m => { brainMemoryCache[m.id] = m; });
+ if (pinnedFilter === 'pinned') items = items.filter(m => m.pinned);
+ else if (pinnedFilter === 'cold') items = items.filter(m => !m.pinned);
+ if (!items.length) {
+ el.innerHTML = '(Keine Memories β leere DB oder Filter zu eng)';
+ return;
+ }
+ renderBrainList(items, false);
+ } catch (e) {
+ el.innerHTML = `π΄ Brain nicht erreichbar (${e.message})`;
+ }
+ }
+
+ const BRAIN_TYPE_LABELS = {
+ identity: 'IdentitΓ€t', rule: 'Regeln / Werte', preference: 'Praeferenzen',
+ tool: 'Tools', skill: 'Skills', fact: 'Fakten',
+ conversation: 'Konversation', reminder: 'Reminder'
+ };
+ const BRAIN_TYPE_ORDER = ['identity','rule','preference','tool','skill','fact','conversation','reminder'];
+
+ function renderMemoryRow(m, withScore) {
+ const pin = m.pinned ? 'π ' : '';
+ const preview = (m.content || '').slice(0, 140).replace(/\n/g, ' ');
+ const score = withScore && typeof m.score === 'number' ? `${m.score.toFixed(2)} ` : '';
+ const typeBadge = withScore ? `${escapeHtml(BRAIN_TYPE_LABELS[m.type] || m.type)} ` : '';
+ return `
+
+
${typeBadge}${pin}${escapeHtml(m.title || '(ohne Titel)')} ${score}
+ ${m.category ? `[${escapeHtml(m.category)}] ` : ''}
+
+
${escapeHtml(preview)}${m.content && m.content.length > 140 ? '...' : ''}
+
+
β
+
π
+
`;
+ }
+
+ function renderBrainList(items, isSearchResult) {
+ const el = document.getElementById('brain-memory-list');
+ if (!el) return;
+ if (isSearchResult) {
+ // Such-Treffer: in Aehnlichkeits-Reihenfolge, kein Type-Gruppieren
+ const html = items.map(m => renderMemoryRow(m, true)).join('');
+ el.innerHTML = html || '(Keine Treffer)';
+ return;
+ }
+ // Normale Liste: nach Type gruppieren
+ const byType = {};
+ items.forEach(m => { (byType[m.type] = byType[m.type] || []).push(m); });
+ const html = BRAIN_TYPE_ORDER.flatMap(t => {
+ if (!byType[t]) return [];
+ const heading = `${BRAIN_TYPE_LABELS[t] || t} (${byType[t].length})
`;
+ const rows = byType[t].map(m => renderMemoryRow(m, false)).join('');
+ return [heading, rows];
+ }).join('');
+ // Falls Items mit unbekannten Types existieren, hinten dranhaengen
+ const extraTypes = Object.keys(byType).filter(t => !BRAIN_TYPE_ORDER.includes(t));
+ let extra = '';
+ for (const t of extraTypes) {
+ extra += `${escapeHtml(t)} (${byType[t].length})
`;
+ extra += byType[t].map(m => renderMemoryRow(m, false)).join('');
+ }
+ el.innerHTML = (html + extra) || '(Keine bekannten Typen gefunden)';
+ }
+
+ // ββ Memory CRUD βββββββββββββββββββββββββββββββββββ
+
+ function openMemoryModal(id) {
+ const modal = document.getElementById('memory-modal');
+ const titleEl = document.getElementById('memory-modal-title');
+ const idEl = document.getElementById('memory-edit-id');
+ const errEl = document.getElementById('memory-modal-error');
+ errEl.style.display = 'none';
+
+ if (id && brainMemoryCache[id]) {
+ const m = brainMemoryCache[id];
+ titleEl.textContent = 'Memory bearbeiten';
+ idEl.value = id;
+ document.getElementById('memory-type').value = m.type || 'fact';
+ document.getElementById('memory-title').value = m.title || '';
+ document.getElementById('memory-content').value = m.content || '';
+ document.getElementById('memory-category').value = m.category || '';
+ document.getElementById('memory-tags').value = (m.tags || []).join(', ');
+ document.getElementById('memory-pinned').checked = !!m.pinned;
+ } else {
+ titleEl.textContent = 'Neue Memory';
+ idEl.value = '';
+ document.getElementById('memory-type').value = 'fact';
+ document.getElementById('memory-title').value = '';
+ document.getElementById('memory-content').value = '';
+ document.getElementById('memory-category').value = '';
+ document.getElementById('memory-tags').value = '';
+ document.getElementById('memory-pinned').checked = false;
+ }
+ modal.classList.add('open');
+ }
+
+ function closeMemoryModal() {
+ document.getElementById('memory-modal').classList.remove('open');
+ }
+
+ async function saveMemory() {
+ const errEl = document.getElementById('memory-modal-error');
+ errEl.style.display = 'none';
+ const id = document.getElementById('memory-edit-id').value;
+ const type = document.getElementById('memory-type').value;
+ const title = document.getElementById('memory-title').value.trim();
+ const content = document.getElementById('memory-content').value.trim();
+ const category = document.getElementById('memory-category').value.trim();
+ const tags = document.getElementById('memory-tags').value.split(',').map(t => t.trim()).filter(Boolean);
+ const pinned = document.getElementById('memory-pinned').checked;
+
+ if (!title) { errEl.textContent = 'Titel fehlt.'; errEl.style.display = 'block'; return; }
+ if (!content) { errEl.textContent = 'Inhalt fehlt.'; errEl.style.display = 'block'; return; }
+
+ try {
+ let r;
+ if (id) {
+ r = await fetch('/api/brain/memory/update/' + encodeURIComponent(id), {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ title, content, pinned, category, tags }),
+ });
+ } else {
+ r = await fetch('/api/brain/memory/save', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ type, title, content, pinned, category, tags, source: 'manual' }),
+ });
+ }
+ if (!r.ok) {
+ const txt = await r.text();
+ throw new Error('HTTP ' + r.status + ': ' + txt.slice(0, 200));
+ }
+ closeMemoryModal();
+ loadBrainMemoryList();
+ loadBrainStatus();
+ } catch (e) {
+ errEl.textContent = e.message;
+ errEl.style.display = 'block';
+ }
+ }
+
+ async function deleteMemory(id) {
+ const m = brainMemoryCache[id];
+ const label = m ? `"${m.title}" (${m.type})` : id;
+ if (!confirm(`Memory ${label} wirklich lΓΆschen?`)) return;
+ try {
+ const r = await fetch('/api/brain/memory/delete/' + encodeURIComponent(id), { method: 'DELETE' });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ loadBrainMemoryList();
+ loadBrainStatus();
+ } catch (e) {
+ alert('LΓΆschen fehlgeschlagen: ' + e.message);
+ }
+ }
+
+ // snapshotSessions entfernt β aria-core ist raus.
+
+ function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+ }
+
+ function brainExport() {
+ // Browser folgt der Download-Header-Antwort automatisch
+ window.location.href = '/api/brain-export';
+ }
+
+ // ββ Migration aus brain-import/ ββββββββββββββββββββββββ
+ async function runMigration() {
+ const status = document.getElementById('brain-import-files-status');
+ if (!confirm('Migration starten?\n\nParst die Markdown-Dateien aus brain-import/ und schreibt sie als pinned Memory-Punkte in die DB. Bei Re-Run werden nur die Migration-Punkte ersetzt β eigene Memories bleiben.')) return;
+ if (status) status.innerHTML = 'β³ Migriere...';
+ try {
+ const r = await fetch('/api/brain/memory/migrate', { method: 'POST' });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const d = await r.json();
+ if (d.error) throw new Error(d.error);
+ const files = (d.files || []).join(', ');
+ if (status) status.innerHTML = `β ${d.created} Memories erstellt aus: ${files} `;
+ loadBrainMemoryList();
+ loadBrainStatus();
+ } catch (e) {
+ if (status) status.innerHTML = `β ${e.message} `;
+ }
+ }
+
+ async function refreshImportFiles() {
+ const el = document.getElementById('brain-import-files-status');
+ if (!el) return;
+ try {
+ const r = await fetch('/api/brain/memory/import-files');
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const d = await r.json();
+ if (!d.exists) {
+ el.innerHTML = `brain-import/ nicht gemountet (${d.import_dir}) `;
+ return;
+ }
+ if (!d.files.length) {
+ el.innerHTML = 'Keine .md-Dateien in brain-import/ ';
+ return;
+ }
+ const fmt = d.files.map(f => `${f.name} (${(f.size/1024).toFixed(1)}KB)`).join(', ');
+ el.innerHTML = `VerfΓΌgbar: ${escapeHtml(fmt)} `;
+ } catch (e) {
+ el.innerHTML = `Brain nicht erreichbar `;
+ }
+ }
+
+ // ββ Bootstrap Export / Import ββββββββββββββββββββββββββ
+ async function exportBootstrap() {
+ const status = document.getElementById('bootstrap-status');
+ if (status) status.innerHTML = 'β³ Lade...';
+ try {
+ const r = await fetch('/api/brain/memory/export-bootstrap');
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const data = await r.json();
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `aria-bootstrap-${ts}.json`;
+ document.body.appendChild(a); a.click();
+ setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
+ if (status) status.innerHTML = `β ${data.count} pinned Memories exportiert `;
+ } catch (e) {
+ if (status) status.innerHTML = `β ${e.message} `;
+ }
+ }
+
+ async function importBootstrap(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+ const status = document.getElementById('bootstrap-status');
+ try {
+ const text = await file.text();
+ const bundle = JSON.parse(text);
+ if (!Array.isArray(bundle.memories)) throw new Error('Datei hat kein "memories"-Array');
+ if (!confirm(`Bootstrap importieren?\n\n${bundle.memories.length} pinned Memories aus "${file.name}".\n\nALLE aktuell pinned Memories werden ΓΌberschrieben. Cold Memory bleibt unverΓ€ndert.`)) {
+ event.target.value = '';
+ return;
+ }
+ if (status) status.innerHTML = 'β³ Importiere...';
+ const r = await fetch('/api/brain/memory/import-bootstrap', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: text,
+ });
+ if (!r.ok) {
+ const errText = await r.text();
+ throw new Error('HTTP ' + r.status + ': ' + errText.slice(0, 200));
+ }
+ const d = await r.json();
+ if (status) status.innerHTML = `β ${d.created} Memories importiert `;
+ loadBrainMemoryList();
+ loadBrainStatus();
+ } catch (e) {
+ if (status) status.innerHTML = `β ${e.message} `;
+ } finally {
+ event.target.value = '';
+ }
+ }
+
+ async function brainImport(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+ const statusEl = document.getElementById('brain-import-status');
+ if (!confirm(`Wirklich importieren? ALLE aktuellen Memories + Skills gehen verloren.\nDatei: ${file.name} (${(file.size/1024/1024).toFixed(1)} MB)`)) {
+ event.target.value = '';
+ return;
+ }
+ statusEl.innerHTML = `β³ Lade hoch (${(file.size/1024/1024).toFixed(1)} MB) β Container werden gestoppt...`;
+ try {
+ const r = await fetch('/api/brain-import', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/gzip' },
+ body: file,
+ });
+ const d = await r.json();
+ if (d.ok) {
+ statusEl.innerHTML = `β ${d.message} `;
+ setTimeout(() => { loadBrainStatus(); loadBrainMemoryList(); }, 3000);
+ } else {
+ statusEl.innerHTML = `β Fehler: ${d.error} `;
+ }
+ } catch (e) {
+ statusEl.innerHTML = `β Upload fehlgeschlagen: ${e.message} `;
+ } finally {
+ event.target.value = '';
}
}
@@ -2522,10 +3326,7 @@
// ββ Einstellungen: OpenClaw Config ββββββββββββββββββββββ
- function loadOpenClawConfig() {
- document.getElementById('openclaw-config').textContent = 'Lade...';
- send({ action: 'get_openclaw_config' });
- }
+ // loadOpenClawConfig entfernt β aria-core ist raus.
// Toggle-Checkbox initial korrekt setzen
const ttsToggleEl = document.getElementById('tts-debug-toggle');
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 067321a..cca1022 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -163,26 +163,10 @@ function traceEnd(ok, detail) {
pendingMessageTime = 0;
}
-// ββ Auto-Restart bei Netzwerk-Namespace-Verlust ββββββ
-// Bei network_mode: "service:aria" verliert dieser Container
-// den Netzwerkzugriff wenn aria-core neustartet.
-// Nach MAX_GATEWAY_FAILURES aufeinanderfolgenden Fehlern β process.exit
-// Docker restart: unless-stopped startet uns mit neuem Namespace neu.
-const MAX_GATEWAY_FAILURES = 6; // 6 Γ 5s = 30s
+// Auto-Restart-Heuristik entfernt (war an aria-core-Network gebunden).
+// connectGateway ist ein No-Op solange der Brain-Loop noch nicht steht.
let gatewayFailCount = 0;
-
-function checkGatewayHealth() {
- if (state.gateway.status === "connected") {
- gatewayFailCount = 0;
- return;
- }
- gatewayFailCount++;
- if (gatewayFailCount >= MAX_GATEWAY_FAILURES) {
- log("error", "server", `Gateway ${MAX_GATEWAY_FAILURES}x nicht erreichbar β Neustart (Netzwerk-Namespace veraltet?)`);
- // Kurze Verzoegerung damit die Log-Nachricht noch gesendet wird
- setTimeout(() => process.exit(1), 500);
- }
-}
+function checkGatewayHealth() { /* No-Op */ }
function nextReqId() {
return `diag-${++reqIdCounter}`;
@@ -213,14 +197,15 @@ function broadcastState() {
// ββ OpenClaw Gateway Verbindung βββββββββββββββββββββββββ
async function connectGateway() {
- if (gatewayWs) {
- try { gatewayWs.close(); } catch (_) {}
- gatewayWs = null;
- }
-
- state.gateway.status = "connecting";
- state.gateway.handshakeOk = false;
+ // aria-core/OpenClaw-Gateway abgerissen β diese Funktion ist ein No-Op,
+ // bis der neue Brain-Loop angedockt ist. Wir setzen den Status nur einmal.
+ state.gateway.status = "disabled";
+ state.gateway.lastError = "aria-core entfernt β Brain-Loop in Arbeit";
broadcastState();
+ return;
+
+ // Originaler Connect-Code unten ist toter Code, bleibt zur Referenz.
+ // eslint-disable-next-line no-unreachable
log("info", "gateway", `Verbinde: ${GATEWAY_URL}`);
try {
@@ -1113,6 +1098,44 @@ async function handleDockerLogs(ws, tab, tail) {
// ββ Docker Exec (Befehl in Container ausfuehren) ββββββββ
+function dockerContainerStop(name, timeoutSec = 10) {
+ return new Promise((resolve, reject) => {
+ const req = http.request({
+ socketPath: "/var/run/docker.sock",
+ path: `/containers/${name}/stop?t=${timeoutSec}`,
+ method: "POST",
+ headers: { "Content-Length": 0 },
+ timeout: (timeoutSec + 5) * 1000,
+ }, (dRes) => {
+ // 204 = OK, 304 = already stopped, 404 = nicht da
+ if (dRes.statusCode === 204 || dRes.statusCode === 304) resolve();
+ else if (dRes.statusCode === 404) resolve(); // Container fehlt: nicht fatal
+ else reject(new Error(`docker stop ${name}: HTTP ${dRes.statusCode}`));
+ dRes.resume();
+ });
+ req.on("error", reject);
+ req.end();
+ });
+}
+
+function dockerContainerStart(name) {
+ return new Promise((resolve, reject) => {
+ const req = http.request({
+ socketPath: "/var/run/docker.sock",
+ path: `/containers/${name}/start`,
+ method: "POST",
+ headers: { "Content-Length": 0 },
+ timeout: 30000,
+ }, (dRes) => {
+ if (dRes.statusCode === 204 || dRes.statusCode === 304) resolve();
+ else reject(new Error(`docker start ${name}: HTTP ${dRes.statusCode}`));
+ dRes.resume();
+ });
+ req.on("error", reject);
+ req.end();
+ });
+}
+
function dockerExec(containerName, cmd) {
return new Promise((resolve, reject) => {
const createBody = JSON.stringify({
@@ -1184,17 +1207,9 @@ function waitForMessage(ws, timeoutMs) {
});
}
-// ββ Watchdog: Stuck Run Erkennung ββββββββββββββββββββββββ
-
-let lastAgentActivity = Date.now();
-let watchdogWarned = false;
-let watchdogFixAttempted = false;
-let pendingMessageTime = 0; // Wann wurde die letzte Nachricht gesendet
-
-function updateAgentActivity() {
- lastAgentActivity = Date.now();
- watchdogWarned = false;
-}
+// ββ Watchdog entfernt β pendingMessageTime bleibt fuer /api/cancel
+let pendingMessageTime = 0;
+function updateAgentActivity() { /* No-Op nach Watchdog-Ausbau */ }
// ββ Disk-Space Monitor βββββββββββββββββββββββββββββββ
// Prueft regelmaessig die Host-Disk (via gemountetem /shared) und
@@ -1243,51 +1258,8 @@ function checkDiskSpace() {
setTimeout(checkDiskSpace, 2000);
setInterval(checkDiskSpace, 30000);
-// Watchdog prΓΌft alle 30s ob ARIA nach einer gesendeten Nachricht reagiert
-setInterval(async () => {
- if (pendingMessageTime === 0) return; // Keine Nachricht gesendet
- const waitingMs = Date.now() - pendingMessageTime;
-
- // Nach 2min ohne Agent-Activity: Warnung
- if (waitingMs > 120000 && !watchdogWarned) {
- watchdogWarned = true;
- log("warn", "server", `Watchdog: Keine ARIA-Aktivitaet seit ${Math.round(waitingMs / 1000)}s β moeglicherweise stuck`);
- broadcast({ type: "watchdog", status: "warning", waitingMs, message: "ARIA reagiert nicht β moeglicherweise stuck Run" });
- }
-
- // Nach 5min: doctor --fix
- if (waitingMs > 300000 && watchdogWarned && !watchdogFixAttempted) {
- watchdogFixAttempted = true;
- log("error", "server", "Watchdog: 5min ohne Antwort β fuehre openclaw doctor --fix aus");
- broadcast({ type: "watchdog", status: "fixing", message: "Auto-Fix: openclaw doctor --fix" });
- try {
- await dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true");
- log("info", "server", "Watchdog: doctor --fix ausgefuehrt");
- broadcast({ type: "watchdog", status: "fixed", message: "doctor --fix ausgefuehrt β warte auf Antwort..." });
- } catch (err) {
- log("error", "server", `Watchdog: doctor --fix fehlgeschlagen: ${err.message}`);
- }
- }
-
- // Nach 8min: Container neustarten
- if (waitingMs > 480000 && watchdogFixAttempted) {
- log("error", "server", "Watchdog: 8min ohne Antwort β starte aria-core + aria-proxy neu");
- broadcast({ type: "watchdog", status: "restarting", message: "Container-Restart: aria-core + aria-proxy" });
- try {
- const { execSync } = require("child_process");
- execSync("docker restart aria-core aria-proxy", { timeout: 60000 });
- log("info", "server", "Watchdog: Container neugestartet");
- broadcast({ type: "watchdog", status: "restarted", message: "Container neugestartet β warte auf Gateway-Reconnect..." });
- // Gateway wird sich automatisch neu verbinden
- } catch (err) {
- log("error", "server", `Watchdog: Container-Restart fehlgeschlagen: ${err.message}`);
- broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: ${err.message}` });
- }
- pendingMessageTime = 0;
- watchdogWarned = false;
- watchdogFixAttempted = false;
- }
-}, 30000);
+// Watchdog entfernt β war auf aria-core/OpenClaw zugeschnitten (doctor --fix,
+// docker restart aria-core). Der Brain-Loop bekommt seinen eigenen Health-Check.
// ββ HTTP Server + WebSocket fuer Browser ββββββββββββββββ
@@ -1335,103 +1307,329 @@ const server = http.createServer((req, res) => {
} else if (req.url === "/api/cancel" && req.method === "POST") {
log("warn", "server", "HTTP /api/cancel β Cancel-Request (von Bridge)");
pendingMessageTime = 0;
- watchdogWarned = false;
- watchdogFixAttempted = false;
if (traceActive) traceEnd(false, "Vom Benutzer abgebrochen (App)");
else broadcast({ type: "agent_activity", activity: "idle" });
- dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {});
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
- } else if (req.url === "/api/doctor-fix" && req.method === "POST") {
- // Manueller "ARIA reparieren"-Button β stuck OpenClaw-Runs aufloesen.
- log("info", "server", "HTTP /api/doctor-fix β manueller Reparatur-Trigger");
- dockerExec("aria-core", "openclaw doctor --fix 2>&1")
- .then(out => {
- const summary = (out || "").split("\n").filter(l => l.trim()).slice(-3).join(" | ");
- broadcast({ type: "watchdog", status: "fixed", message: `Reparatur ausgefuehrt: ${summary || "OK"}` });
- res.writeHead(200, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ ok: true, output: out }));
- })
- .catch(err => {
- broadcast({ type: "watchdog", status: "error", message: `Reparatur fehlgeschlagen: ${err.message}` });
- res.writeHead(500, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ ok: false, error: err.message }));
- });
- return;
- } else if (req.url === "/api/aria-session-reset" && req.method === "POST") {
- // Sessions weg + Container neu β fuer Compact-After-N-Messages.
- // E2BIG bei zu langen Sessions: argv beim Subprocess-spawn ueberschritten.
- log("warn", "server", "HTTP /api/aria-session-reset β Sessions loeschen + Restart");
- broadcast({ type: "watchdog", status: "fixing", message: "Sessions werden geleert β ARIA bekommt frischen Start" });
- dockerExec("aria-core", "rm -f /home/node/.openclaw/agents/main/sessions/*.jsonl /home/node/.openclaw/agents/main/sessions/*.lock 2>&1 && echo '{}' > /home/node/.openclaw/agents/main/sessions/sessions.json")
- .then(() => {
- // Restart via Docker-API (gleicher Pfad wie /api/aria-restart)
- const restartReq = http.request({
- socketPath: "/var/run/docker.sock",
- path: "/containers/aria-core/restart?t=10",
- method: "POST",
- headers: { "Content-Length": 0 },
- timeout: 30000,
- }, (dRes) => {
- if (dRes.statusCode === 204) {
- log("info", "server", "aria-session-reset OK");
- broadcast({ type: "watchdog", status: "fixed", message: "Sessions geleert, ARIA neu gestartet" });
- res.writeHead(200, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ ok: true }));
- } else {
- res.writeHead(500, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ ok: false, error: `Docker-API ${dRes.statusCode}` }));
- }
- });
- restartReq.on("error", (err) => {
- res.writeHead(500, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ ok: false, error: err.message }));
- });
- restartReq.end();
- })
- .catch((err) => {
- log("error", "server", `aria-session-reset Cleanup fehlgeschlagen: ${err.message}`);
- res.writeHead(500, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ ok: false, error: err.message }));
- });
- return;
- } else if (req.url === "/api/aria-restart" && req.method === "POST") {
- // Harter Restart β fuer Faelle wo doctor --fix nicht reicht (alive aber
- // haengender Run). Geht ueber Docker-API (Socket), kein CLI noetig.
- // POST /containers/aria-core/restart?t=10 β SIGTERM, dann nach 10s SIGKILL.
- log("warn", "server", "HTTP /api/aria-restart β harter Container-Restart");
- broadcast({ type: "watchdog", status: "fixing", message: "ARIA wird hart neu gestartet (~15s)" });
- const restartReq = http.request({
- socketPath: "/var/run/docker.sock",
- path: "/containers/aria-core/restart?t=10",
- method: "POST",
- headers: { "Content-Length": 0 },
- timeout: 30000,
- }, (dRes) => {
- let body = "";
- dRes.on("data", (c) => body += c);
- dRes.on("end", () => {
- // Docker-API: 204 = OK, 404 = container nicht da, 500 = anderer Fehler
- if (dRes.statusCode === 204) {
- log("info", "server", "aria-restart OK (Docker-API)");
- broadcast({ type: "watchdog", status: "fixed", message: "ARIA wurde neu gestartet β sollte gleich antworten" });
- res.writeHead(200, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ ok: true }));
- } else {
- log("error", "server", `aria-restart Docker-API ${dRes.statusCode}: ${body.slice(0, 200)}`);
- broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: HTTP ${dRes.statusCode}` });
- res.writeHead(500, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ ok: false, error: `Docker-API ${dRes.statusCode}: ${body}` }));
- }
- });
- });
- restartReq.on("error", (err) => {
- log("error", "server", `aria-restart Socket-Fehler: ${err.message}`);
- broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: ${err.message}` });
+ } else if (req.url === "/api/files-list" && req.method === "GET") {
+ // Liste alle Dateien in /shared/uploads/ β die kommen entweder vom User
+ // (Upload aus App/Diagnostic) oder von ARIA (aria_. Pattern).
+ try {
+ const dir = "/shared/uploads";
+ let entries = [];
+ try { entries = fs.readdirSync(dir); } catch { entries = []; }
+ const files = entries
+ .map(name => {
+ try {
+ const full = path.join(dir, name);
+ const st = fs.statSync(full);
+ if (!st.isFile()) return null;
+ return {
+ name,
+ path: full,
+ size: st.size,
+ mtime: Math.floor(st.mtimeMs),
+ fromAria: name.startsWith("aria_"),
+ };
+ } catch { return null; }
+ })
+ .filter(Boolean)
+ .sort((a, b) => b.mtime - a.mtime);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: true, files }));
+ } catch (err) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: err.message }));
+ }
+ return;
+ } else if (req.url.startsWith("/api/files-download?") && req.method === "GET") {
+ const u = new URL("http://x" + req.url);
+ const p = u.searchParams.get("path") || "";
+ const safe = path.resolve(p);
+ if (!safe.startsWith("/shared/uploads/") || !fs.existsSync(safe)) {
+ res.writeHead(404, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: "Datei nicht gefunden" }));
+ return;
+ }
+ const stat = fs.statSync(safe);
+ const fname = path.basename(safe);
+ res.writeHead(200, {
+ "Content-Type": "application/octet-stream",
+ "Content-Length": stat.size,
+ "Content-Disposition": `attachment; filename="${fname}"`,
});
- restartReq.end();
+ fs.createReadStream(safe).pipe(res);
+ return;
+ } else if (req.url === "/api/files-delete" && req.method === "POST") {
+ let body = "";
+ req.on("data", c => { body += c; if (body.length > 4096) req.destroy(); });
+ req.on("end", () => {
+ try {
+ const { path: p } = JSON.parse(body || "{}");
+ const safe = path.resolve(p || "");
+ if (!safe.startsWith("/shared/uploads/")) {
+ res.writeHead(400, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: "Pfad nicht erlaubt" }));
+ return;
+ }
+ if (!fs.existsSync(safe)) {
+ res.writeHead(404, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: "Datei nicht vorhanden" }));
+ return;
+ }
+ fs.unlinkSync(safe);
+ log("info", "server", `Datei geloescht: ${safe}`);
+ // Live-Event an alle Browser-Clients
+ broadcast({ type: "file_deleted", path: safe });
+ // Auch an die Bridge weiterleiten, damit die App-Bubbles aktualisiert
+ sendToRVS_raw({
+ type: "file_deleted",
+ payload: { path: safe },
+ timestamp: Date.now(),
+ });
+ // Im chat_backup.jsonl markieren β beim Reload sehen Clients dass weg
+ try {
+ const backupFile = "/shared/config/chat_backup.jsonl";
+ const line = JSON.stringify({
+ type: "file_deleted",
+ path: safe,
+ ts: Date.now(),
+ by: "user",
+ }) + "\n";
+ fs.appendFileSync(backupFile, line);
+ } catch {}
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: true }));
+ } catch (err) {
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: err.message }));
+ }
+ });
+ return;
+ } else if (req.url === "/api/voice-config-export" && req.method === "GET") {
+ // voice_config.json + highlight_triggers.json als JSON-Bundle exportieren
+ try {
+ const bundle = {};
+ try { bundle.voice_config = JSON.parse(fs.readFileSync("/shared/config/voice_config.json", "utf-8")); } catch {}
+ try { bundle.highlight_triggers = JSON.parse(fs.readFileSync("/shared/config/highlight_triggers.json", "utf-8")); } catch {}
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
+ res.writeHead(200, {
+ "Content-Type": "application/json",
+ "Content-Disposition": `attachment; filename="aria-voice-settings-${ts}.json"`,
+ });
+ res.end(JSON.stringify(bundle, null, 2));
+ } catch (err) {
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: err.message }));
+ }
+ return;
+ } else if (req.url === "/api/voice-config-import" && req.method === "POST") {
+ let body = "";
+ req.on("data", c => { body += c; if (body.length > 1024 * 1024) req.destroy(); });
+ req.on("end", () => {
+ try {
+ const bundle = JSON.parse(body || "{}");
+ fs.mkdirSync("/shared/config", { recursive: true });
+ if (bundle.voice_config && typeof bundle.voice_config === "object") {
+ fs.writeFileSync("/shared/config/voice_config.json", JSON.stringify(bundle.voice_config, null, 2));
+ }
+ if (bundle.highlight_triggers && typeof bundle.highlight_triggers === "object") {
+ fs.writeFileSync("/shared/config/highlight_triggers.json", JSON.stringify(bundle.highlight_triggers, null, 2));
+ }
+ log("info", "server", "Voice-Settings importiert");
+ // Bridge bekommt die neue Config via RVS (bei naechstem Reload), aber
+ // ein Restart ist sauberer damit Whisper/F5 sofort neu laden.
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: true, message: "Voice-Settings importiert β Bridge-Restart empfohlen." }));
+ } catch (err) {
+ res.writeHead(400, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: err.message }));
+ }
+ });
+ return;
+ } else if (req.url === "/api/wipe-all" && req.method === "POST") {
+ // Komplett-Reset β Gedaechtnis, Stimmen, Config alle weg. SSH-Keys
+ // und .env bleiben, RVS-Anbindung bleibt. Brain + Qdrant werden
+ // gestoppt, Filesystem geleert, dann neu gestartet.
+ log("warn", "server", "HTTP /api/wipe-all β Gesamt-Reset");
+ const { spawn } = require("child_process");
+
+ Promise.resolve()
+ .then(() => dockerContainerStop("aria-brain").catch(() => {}))
+ .then(() => dockerContainerStop("aria-qdrant").catch(() => {}))
+ .then(() => new Promise((resolve, reject) => {
+ // Liste der Pfade die innerhalb des Diagnostic-Containers gemountet
+ // sind: /shared (config + voices) und /brain (Memory + Skills).
+ const cmd = [
+ "rm -rf /shared/config /shared/voices /shared/conversations-archive /shared/chat_backup.jsonl",
+ "rm -rf /brain/data /brain/qdrant /brain/.[!.]* 2>/dev/null || true",
+ "mkdir -p /brain/data /brain/qdrant /shared/config /shared/voices",
+ ].join(" && ");
+ const sh = spawn("sh", ["-c", cmd]);
+ let stderr = "";
+ sh.stderr.on("data", d => stderr += d.toString());
+ sh.on("close", c => c === 0 ? resolve() : reject(new Error(`wipe exit ${c}: ${stderr}`)));
+ }))
+ .then(async () => {
+ // Bridge auch neustarten, damit Whisper-Config + Voice-Config frisch
+ await dockerContainerStop("aria-bridge").catch(() => {});
+ await dockerContainerStart("aria-qdrant");
+ await new Promise(r => setTimeout(r, 2000));
+ await dockerContainerStart("aria-brain");
+ await dockerContainerStart("aria-bridge");
+ log("info", "server", "wipe-all OK β Brain + Qdrant + Bridge neu gestartet");
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: true, message: "Komplett-Reset durchgefuehrt β ARIA ist leer." }));
+ })
+ .catch(async err => {
+ log("error", "server", `wipe-all: ${err.message}`);
+ // Container trotzdem hochfahren
+ try { await dockerContainerStart("aria-qdrant"); } catch {}
+ try { await dockerContainerStart("aria-brain"); } catch {}
+ try { await dockerContainerStart("aria-bridge"); } catch {}
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: err.message }));
+ });
+ return;
+ } else if (req.url === "/api/container-restart" && req.method === "POST") {
+ // Generischer Restart fuer ARIAs Container β Whitelist verhindert
+ // dass jemand aria-proxy oder das Diagnostic selbst kickt.
+ let body = "";
+ req.on("data", c => { body += c; if (body.length > 1024) req.destroy(); });
+ req.on("end", async () => {
+ try {
+ const { name } = JSON.parse(body || "{}");
+ const ALLOWED = ["aria-bridge", "aria-brain", "aria-qdrant"];
+ if (!ALLOWED.includes(name)) {
+ res.writeHead(400, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: `Container '${name}' nicht erlaubt (Whitelist: ${ALLOWED.join(", ")})` }));
+ return;
+ }
+ log("info", "server", `HTTP /api/container-restart β ${name}`);
+ await dockerContainerStop(name).catch(() => {});
+ await new Promise(r => setTimeout(r, 500));
+ await dockerContainerStart(name);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: true, container: name }));
+ } catch (err) {
+ log("error", "server", `container-restart: ${err.message}`);
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: err.message }));
+ }
+ });
+ return;
+ } else if (req.url.startsWith("/api/brain/")) {
+ // Reverse-Proxy zum aria-brain Container (intern auf 8080, nicht expose'd).
+ // Frontend ruft z.B. /api/brain/health β http://aria-brain:8080/health
+ const targetPath = req.url.replace(/^\/api\/brain/, "");
+ const proxyReq = http.request({
+ host: "aria-brain",
+ port: 8080,
+ path: targetPath,
+ method: req.method,
+ headers: req.headers,
+ timeout: 30000,
+ }, (proxyRes) => {
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
+ proxyRes.pipe(res);
+ });
+ proxyReq.on("error", (err) => {
+ res.writeHead(503, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ status: "unreachable", error: err.message }));
+ });
+ req.pipe(proxyReq);
+ return;
+ } else if (req.url === "/api/brain-export" && req.method === "GET") {
+ // Komplettes Gehirn als tar.gz streamen.
+ // Schritte: Brain + Qdrant stoppen (saubere Bytes) β tar streamen β wieder starten.
+ log("info", "server", "HTTP /api/brain-export β Gehirn-Export gestartet");
+ const { spawn } = require("child_process");
+
+ dockerContainerStop("aria-brain").catch(() => {})
+ .then(() => dockerContainerStop("aria-qdrant").catch(() => {}))
+ .then(() => {
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
+ res.writeHead(200, {
+ "Content-Type": "application/gzip",
+ "Content-Disposition": `attachment; filename="aria-brain-${ts}.tar.gz"`,
+ "Cache-Control": "no-store",
+ });
+ const tar = spawn("tar", ["czf", "-", "-C", "/brain", "."]);
+ tar.stdout.pipe(res);
+ let stderr = "";
+ tar.stderr.on("data", d => stderr += d.toString());
+ const restartAll = async () => {
+ try {
+ await dockerContainerStart("aria-qdrant");
+ await new Promise(r => setTimeout(r, 1500));
+ await dockerContainerStart("aria-brain");
+ log("info", "server", "brain-export: Container wieder gestartet");
+ } catch (e) {
+ log("error", "server", `brain-export Restart fehlgeschlagen: ${e.message}`);
+ }
+ };
+ tar.on("close", (code) => {
+ if (code !== 0) log("error", "server", `brain-export tar exit ${code}: ${stderr.slice(0, 200)}`);
+ restartAll();
+ });
+ // Client-Disconnect β tar abbrechen + Container wieder hoch
+ req.on("close", () => { if (!tar.killed) { tar.kill("SIGTERM"); restartAll(); } });
+ })
+ .catch(err => {
+ log("error", "server", `brain-export: ${err.message}`);
+ if (!res.headersSent) {
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: err.message }));
+ }
+ });
+ return;
+ } else if (req.url === "/api/brain-import" && req.method === "POST") {
+ // Body ist die rohe tar.gz-Datei (kein multipart). Beliebig grosse Uploads
+ // werden direkt in tar -xz gepiped β kein Memory-Bloat.
+ log("warn", "server", "HTTP /api/brain-import β Gehirn-Import gestartet");
+ const { spawn } = require("child_process");
+
+ dockerContainerStop("aria-brain").catch(() => {})
+ .then(() => dockerContainerStop("aria-qdrant").catch(() => {}))
+ .then(() => new Promise((resolve, reject) => {
+ // Brain-Verzeichnis leeren (Mount-Point selbst nicht entfernen)
+ const rm = spawn("sh", ["-c", "rm -rf /brain/data /brain/qdrant /brain/.[!.]* 2>/dev/null; mkdir -p /brain/data /brain/qdrant"]);
+ rm.on("close", (code) => code === 0 ? resolve() : reject(new Error(`cleanup exit ${code}`)));
+ rm.on("error", reject);
+ }))
+ .then(() => new Promise((resolve, reject) => {
+ const tar = spawn("tar", ["xzf", "-", "-C", "/brain"]);
+ req.pipe(tar.stdin);
+ let stderr = "";
+ tar.stderr.on("data", d => stderr += d.toString());
+ tar.on("close", (code) => {
+ if (code === 0) resolve();
+ else reject(new Error(`tar exit ${code}: ${stderr.slice(0, 300)}`));
+ });
+ tar.on("error", reject);
+ // Falls Client mittendrin abbricht
+ req.on("close", () => { if (!tar.killed && !req.complete) tar.kill("SIGTERM"); });
+ }))
+ .then(async () => {
+ // Subdirs sicherstellen (falls Archiv unsauber war)
+ await new Promise(r => spawn("mkdir", ["-p", "/brain/data", "/brain/qdrant"]).on("close", r));
+ await dockerContainerStart("aria-qdrant");
+ await new Promise(r => setTimeout(r, 2000));
+ await dockerContainerStart("aria-brain");
+ log("info", "server", "brain-import OK β Container neu gestartet");
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: true, message: "Gehirn importiert, Container neu gestartet" }));
+ })
+ .catch(async (err) => {
+ log("error", "server", `brain-import fehlgeschlagen: ${err.message}`);
+ // Container trotzdem wieder hochfahren, sonst steht alles
+ try { await dockerContainerStart("aria-qdrant"); } catch {}
+ try { await dockerContainerStart("aria-brain"); } catch {}
+ if (!res.headersSent) {
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: err.message }));
+ }
+ });
return;
} else if (req.url.startsWith("/shared/")) {
// Dateien aus Shared Volume ausliefern (Bilder, Uploads)
@@ -1531,6 +1729,12 @@ wss.on("connection", (ws) => {
} else if (msg.action === "xtts_list_voices") {
// Frische Verbindung die auf Antwort wartet
sendToRVS_withResponse("xtts_list_voices", {}, "xtts_voices_list", ws);
+ } else if (msg.action === "xtts_export_voice") {
+ // Anfrage an XTTS-Bridge β gibt die Stimme als base64 tar.gz zurueck
+ sendToRVS_withResponse("xtts_export_voice", { name: msg.name }, "xtts_voice_exported", ws);
+ } else if (msg.action === "xtts_import_voice") {
+ // tar.gz (base64) an XTTS-Bridge schicken β die packt aus
+ sendToRVS_withResponse("xtts_import_voice", { name: msg.name, data: msg.data }, "xtts_voice_imported", ws);
} else if (msg.action === "xtts_delete_voice") {
// Weiterleiten an XTTS-Bridge, die antwortet mit neuer Liste
sendToRVS_raw({ type: "xtts_delete_voice", payload: { name: msg.name }, timestamp: Date.now() });
@@ -1577,35 +1781,19 @@ wss.on("connection", (ws) => {
checkDesktopAvailable(ws);
} else if (msg.action === "load_chat_history") {
handleLoadChatHistory(ws);
- } else if (msg.action === "list_sessions") {
- handleListSessions(ws);
- } else if (msg.action === "read_session") {
- handleReadSession(ws, msg.sessionPath);
- } else if (msg.action === "export_session") {
- handleExportSession(ws, msg.sessionPath, msg.sessionKey);
- } else if (msg.action === "delete_session") {
- handleDeleteSession(ws, msg.sessionPath);
- } else if (msg.action === "set_active_session") {
- handleSetActiveSession(ws, msg.sessionKey);
- } else if (msg.action === "get_active_session") {
- ws.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
- } else if (msg.action === "create_session") {
- handleCreateSession(ws, msg.sessionName);
+ // Sessions- und Brain-File-Viewer entfernt β Sessions sind raus, Memory
+ // laeuft jetzt komplett ueber die Vector-DB im aria-brain (siehe Gehirn-Tab).
+ // restart_session kommt weiter rein, weil der Watchdog ihn manchmal triggert.
} else if (msg.action === "restart_session") {
handleRestartSession(ws);
- } else if (msg.action === "list_brain") {
- handleListBrain(ws);
- } else if (msg.action === "read_brain_file") {
- handleReadBrainFile(ws, msg.filename);
// ββ Einstellungen ββ
// list_permissions / save_permissions entfernt β Alles-oder-Nichts via --dangerously-skip-permissions
} else if (msg.action === "get_model") {
handleGetModel(ws);
} else if (msg.action === "set_model") {
handleSetModel(ws, msg.model);
- } else if (msg.action === "get_openclaw_config") {
- handleGetOpenClawConfig(ws);
}
+ // get_openclaw_config entfernt β aria-core ist raus.
} catch {}
});
@@ -1838,446 +2026,89 @@ function checkDesktopAvailable(clientWs) {
});
}
-// ββ Session Viewer ββββββββββββββββββββββββββββββββββββββ
+// ββ Session-Viewer + Brain-File-Viewer entfernt βββββββββ
+// OpenClaw-Sessions sind raus. Wer alte jsonl-Konversationen sichern
+// will: POST /api/sessions-snapshot kopiert sie nach
+// /shared/conversations-archive/ β spaeter Brain-Migration.
+// Memory laeuft jetzt komplett ueber den aria-brain Container
+// (Vector-DB, siehe /api/brain/* Proxy).
-const SESSIONS_DIR = "/home/node/.openclaw/agents/main/sessions";
-
-async function handleListSessions(clientWs) {
- try {
- log("info", "server", "Lade Sessions aus aria-core...");
-
- // sessions.json als Index lesen + Datei-Details holen (inkl. .reset.* Archive)
- const raw = await dockerExec("aria-core", `
- cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}' &&
- echo '===FILE_DETAILS===' &&
- for f in ${SESSIONS_DIR}/*.jsonl ${SESSIONS_DIR}/*.jsonl.reset.*; do
- [ -f "$f" ] || continue
- name=$(basename "$f")
- msgs=$(grep -cE '"role":"(user|assistant)"' "$f" 2>/dev/null || echo 0)
- size=$(du -h "$f" 2>/dev/null | cut -f1)
- modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0)
- echo "FILE:$name|LINES:$msgs|SIZE:$size|MODIFIED:$modified"
- done
- `.trim());
-
- const parts = raw.split("===FILE_DETAILS===");
- let sessionsIndex = {};
- try { sessionsIndex = JSON.parse(parts[0].trim()); } catch {}
-
- // Datei-Details parsen
- const fileDetails = {};
- if (parts[1]) {
- for (const line of parts[1].trim().split("\n")) {
- if (!line.startsWith("FILE:")) continue;
- const segs = {};
- for (const seg of line.split("|")) {
- const idx = seg.indexOf(":");
- if (idx > 0) segs[seg.slice(0, idx)] = seg.slice(idx + 1);
- }
- if (segs.FILE) fileDetails[segs.FILE] = segs;
- }
- }
-
- // Sessions zusammenbauen: Index + Datei-Info kombinieren
- const sessions = [];
-
- // Aus sessions.json die Session-Keys und IDs
- const indexEntries = Array.isArray(sessionsIndex) ? sessionsIndex
- : Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions
- : Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) }));
-
- for (const entry of indexEntries) {
- const id = entry.id || entry.sessionId || "";
- const rawKey = entry.key || entry.sessionKey || entry.name || id;
- // "agent:main:aria-diagnostic" β "aria-diagnostic"
- const key = rawKey.replace(/^agent:main:/, "");
- const filename = `${id}.jsonl`;
- const details = fileDetails[filename] || {};
- // updatedAt aus sessions.json (ms) ist genauer als stat
- const updatedAt = entry.updatedAt || 0;
- const model = entry.model || "";
-
- sessions.push({
- path: `${SESSIONS_DIR}/${filename}`,
- sessionKey: key,
- sessionId: id,
- lines: parseInt(details.LINES) || 0,
- size: details.SIZE || "?",
- modified: updatedAt ? Math.floor(updatedAt / 1000) : (parseInt(details.MODIFIED) || 0),
- model,
- });
-
- // Aus fileDetails entfernen (fuer Waisen-Check)
- delete fileDetails[filename];
- }
-
- // Dateien die nicht im Index stehen (Waisen ODER Reset-Archive)
- for (const [filename, details] of Object.entries(fileDetails)) {
- // .jsonl.reset.Z β archivierte Session (OpenClaw-Reset)
- // Format: 528f4d70-...jsonl.reset.2026-04-18T09-49-44.814Z
- const resetMatch = filename.match(/^([a-f0-9-]+)\.jsonl\.reset\.(.+Z)$/);
- if (resetMatch) {
- const id = resetMatch[1];
- // Timestamp ISO-8601 parsen: 2026-04-18T09-49-44.814Z β 2026-04-18T09:49:44.814Z
- const tsStr = resetMatch[2].replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3");
- const resetAt = Math.floor(new Date(tsStr).getTime() / 1000) || parseInt(details.MODIFIED) || 0;
- sessions.push({
- path: `${SESSIONS_DIR}/${filename}`,
- sessionKey: id.slice(0, 8) + "β¦ (archiv)",
- sessionId: id,
- lines: parseInt(details.LINES) || 0,
- size: details.SIZE || "?",
- modified: resetAt,
- archived: true,
- resetAt,
- });
- continue;
- }
- // Echte Waisen (UUID.jsonl ohne Eintrag in sessions.json)
- const id = filename.replace(".jsonl", "");
- sessions.push({
- path: `${SESSIONS_DIR}/${filename}`,
- sessionKey: id.slice(0, 12) + "...",
- sessionId: id,
- lines: parseInt(details.LINES) || 0,
- size: details.SIZE || "?",
- modified: parseInt(details.MODIFIED) || 0,
- orphan: true,
- });
- }
-
- sessions.sort((a, b) => b.modified - a.modified);
-
- clientWs.send(JSON.stringify({ type: "sessions_list", sessions }));
- log("info", "server", `${sessions.length} Session(s) gefunden`);
- } catch (err) {
- log("error", "server", `Sessions laden fehlgeschlagen: ${err.message}`);
- clientWs.send(JSON.stringify({ type: "sessions_list", sessions: [], error: err.message }));
- }
-}
-
-async function handleReadSession(clientWs, sessionPath) {
- if (!sessionPath || sessionPath.includes("..")) {
- clientWs.send(JSON.stringify({ type: "session_detail", error: "Ungueltiger Pfad" }));
- return;
- }
- try {
- // Letzte 100 Zeilen der Session (JSONL)
- const raw = await dockerExec("aria-core", `tail -100 '${sessionPath.replace(/'/g, "")}'`);
- const messages = [];
- for (const line of raw.split("\n")) {
- if (!line.trim()) continue;
- try {
- const obj = JSON.parse(line);
- messages.push(obj);
- } catch {}
- }
- clientWs.send(JSON.stringify({ type: "session_detail", path: sessionPath, messages, raw: messages.length === 0 ? raw : undefined }));
- } catch (err) {
- clientWs.send(JSON.stringify({ type: "session_detail", error: err.message }));
- }
-}
-
-async function handleExportSession(clientWs, sessionPath, sessionKey) {
- if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith(SESSIONS_DIR)) {
- clientWs.send(JSON.stringify({ type: "session_export", ok: false, error: "Ungueltiger Pfad" }));
- return;
- }
- try {
- const safePath = sessionPath.replace(/'/g, "");
- const raw = await dockerExec("aria-core", `cat '${safePath}'`);
- const lines = raw.split("\n").filter(l => l.trim());
-
- const blocks = [];
- for (const line of lines) {
- let obj;
- try { obj = JSON.parse(line); } catch { continue; }
- if (obj.type !== "message" || !obj.message) continue;
- const role = obj.message.role;
- if (role !== "user" && role !== "assistant") continue;
-
- let text = "";
- const content = obj.message.content;
- if (typeof content === "string") text = content;
- else if (Array.isArray(content)) text = content.filter(c => c.type === "text").map(c => c.text || "").join("\n");
- if (!text) continue;
-
- if (role === "user") {
- text = text.replace(/^Sender \(untrusted metadata\):[\s\S]*?```[\s\S]*?```\s*\n*/m, "").trim();
- text = text.replace(/^\[.*?\]\s*/, "").trim();
- } else {
- text = text.replace(/^\[\[reply_to_\w+\]\]\s*/g, "").trim();
- }
- if (!text) continue;
-
- const ts = obj.message.timestamp || obj.timestamp || 0;
- const when = ts ? new Date(ts).toISOString().replace("T", " ").slice(0, 19) : "";
- const heading = role === "user" ? "## π§ User" : "## π€ ARIA";
- blocks.push(`${heading}${when ? ` β ${when}` : ""}\n\n${text}`);
- }
-
- const exportedAt = new Date().toISOString().replace("T", " ").slice(0, 19);
- const title = sessionKey || sessionPath.split("/").pop().replace(".jsonl", "");
- const markdown = [
- `# Session: ${title}`,
- ``,
- `Exportiert: ${exportedAt} `,
- `Quelle: ${sessionPath}`,
- ``,
- `---`,
- ``,
- blocks.join("\n\n---\n\n"),
- ``,
- ].join("\n");
-
- const safeKey = (sessionKey || "session").replace(/[^a-zA-Z0-9_-]/g, "_");
- const filename = `${exportedAt.slice(0, 10)}_${safeKey}.md`;
- clientWs.send(JSON.stringify({ type: "session_export", ok: true, filename, markdown }));
- log("info", "server", `Session exportiert: ${filename} (${blocks.length} Nachrichten)`);
- } catch (err) {
- log("error", "server", `Session-Export fehlgeschlagen: ${err.message}`);
- clientWs.send(JSON.stringify({ type: "session_export", ok: false, error: err.message }));
- }
-}
-
-async function handleDeleteSession(clientWs, sessionPath) {
- if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith(SESSIONS_DIR)) {
- clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: "Ungueltiger Pfad" }));
- return;
- }
- try {
- log("warn", "server", `Loesche Session: ${sessionPath}`);
- const safePath = sessionPath.replace(/'/g, "");
- // Session-ID aus Pfad extrahieren (UUID.jsonl)
- const filename = safePath.split("/").pop();
- const sessionId = filename.replace(".jsonl", "");
-
- // 1. JSONL-Datei loeschen
- await dockerExec("aria-core", `rm -f '${safePath}'`);
-
- // 2. Eintrag aus sessions.json entfernen
- try {
- const sFile = `${SESSIONS_DIR}/sessions.json`;
- const script = [
- 'const fs=require("fs");',
- `const f="${sFile}",sid="${sessionId}";`,
- 'try{const d=JSON.parse(fs.readFileSync(f,"utf8"));',
- 'for(const k of Object.keys(d)){const v=d[k];',
- 'if(v&&(v.sessionId===sid||v.id===sid))delete d[k];}',
- 'fs.writeFileSync(f,JSON.stringify(d,null,2));}catch(e){}',
- ].join("");
- const b64 = Buffer.from(script).toString("base64");
- await dockerExec("aria-core", `echo ${b64} | base64 -d | node`);
- } catch (e) {
- log("warn", "server", `sessions.json Update fehlgeschlagen: ${e.message}`);
- }
-
- clientWs.send(JSON.stringify({ type: "session_deleted", ok: true, path: sessionPath }));
- log("info", "server", "Session geloescht");
- } catch (err) {
- clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: err.message }));
- }
-}
-
-// ββ Session-Aufloesung: letzte aktive Session finden ββββ
-// Wird nach Gateway-(Re-)Connect aufgerufen. Darf die explizit gewaehlte
-// Session NIE ueberschreiben β nur beim absoluten Erststart auto-picken.
-async function resolveActiveSession() {
- if (sessionFromFile) {
- log("info", "server", `Session '${activeSessionKey}' aus /data β keine Auto-Wahl`);
- return;
- }
-
- const indexRaw = await dockerExec("aria-core", `cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}'`);
- log("debug", "server", `sessions.json: ${indexRaw.slice(0, 500)}`);
- let sessionsIndex = {};
- try { sessionsIndex = JSON.parse(indexRaw.trim()); } catch { return; }
-
- const entries = Array.isArray(sessionsIndex) ? sessionsIndex
- : Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions
- : Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) }));
-
- if (entries.length === 0) return;
-
- // Vorhandene Keys loggen
- const keys = entries.map(e => (e.key || e.sessionKey || e.name || "?").replace(/^agent:main:/, ""));
- log("info", "server", `Verfuegbare Sessions: [${keys.join(", ")}]`);
-
- // Neueste Session nehmen β aber user-definierte bevorzugen.
- // aria-bridge / aria-diagnostic werden von den Services auto-erstellt;
- // bei erstem Start soll lieber eine "echte" Session gewaehlt werden,
- // falls vorhanden.
- const AUTO_KEYS = new Set(["aria-bridge", "aria-diagnostic"]);
- const normalise = (e) => (e.key || e.sessionKey || e.name || "").replace(/^agent:main:/, "");
-
- const userEntries = entries.filter(e => !AUTO_KEYS.has(normalise(e)));
- const pool = userEntries.length > 0 ? userEntries : entries;
-
- let newest = null;
- let newestTime = 0;
- for (const entry of pool) {
- const t = entry.updatedAt || entry.createdAt || 0;
- if (t >= newestTime) {
- newestTime = t;
- newest = entry;
- }
- }
-
- if (newest) {
- const key = normalise(newest);
- if (key) {
- activeSessionKey = key;
- persistActiveSession(activeSessionKey);
- log("info", "server", `Auto-Wahl Erststart: '${activeSessionKey}'`);
- for (const c of browserClients) {
- c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
- }
- }
- }
-}
-
-// ββ Chat-History aus aktiver Session laden (Display-Only) ββ
+// ββ Chat-History: liest aus chat_backup.jsonl ββββββββββββ
+// Ablage: jede Zeile ein JSON-Eintrag.
+// {ts, role: "user"|"assistant", text, session}
+// {ts, type: "file_deleted", path, by}
+// Marker [FILE: /shared/uploads/...] werden zu aria_file-Bubbles aufgeloest,
+// wenn die Datei noch existiert; geloeschte Dateien zu deleted-Markern.
async function handleLoadChatHistory(clientWs) {
try {
- // Session-ID fuer den aktiven sessionKey finden
- const indexRaw = await dockerExec("aria-core", `cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}'`);
- let sessionsIndex = {};
- try { sessionsIndex = JSON.parse(indexRaw.trim()); } catch {}
-
- const entries = Array.isArray(sessionsIndex) ? sessionsIndex
- : Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions
- : Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) }));
-
- let sessionId = null;
- const availableKeys = [];
- for (const entry of entries) {
- const rawKey = entry.key || entry.sessionKey || entry.name || "";
- const key = rawKey.replace(/^agent:main:/, "");
- availableKeys.push(key);
- if (key === activeSessionKey) {
- sessionId = entry.id || entry.sessionId || "";
- break;
- }
- }
-
- if (!sessionId) {
- log("warn", "server", `Chat-History: Session '${activeSessionKey}' nicht in sessions.json gefunden. Verfuegbar: [${availableKeys.join(", ")}]`);
+ const file = "/shared/config/chat_backup.jsonl";
+ let raw = "";
+ try { raw = fs.readFileSync(file, "utf-8"); } catch {
clientWs.send(JSON.stringify({ type: "chat_history", messages: [] }));
return;
}
+ const lines = raw.split("\n").filter(l => l.trim());
+ const deletedPaths = new Set();
+ const messages = [];
+ const mimeMap = {
+ ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif",
+ ".webp": "image/webp", ".svg": "image/svg+xml", ".pdf": "application/pdf",
+ ".mp3": "audio/mpeg", ".wav": "audio/wav", ".txt": "text/plain",
+ ".md": "text/markdown", ".json": "application/json", ".zip": "application/zip",
+ };
- log("info", "server", `Chat-History: Session '${activeSessionKey}' -> ID '${sessionId}'`);
-
- const sessionFile = `${SESSIONS_DIR}/${sessionId}.jsonl`;
- const raw = await dockerExec("aria-core", `cat '${sessionFile}' 2>/dev/null || echo ''`);
- const chatMessages = [];
-
- for (const line of raw.split("\n")) {
- if (!line.trim()) continue;
+ // Erst Durchlauf: deleted-Marker sammeln
+ for (const line of lines) {
try {
const obj = JSON.parse(line);
- // OpenClaw Format: {"type":"message","message":{"role":"user|assistant","content":[...]}}
- if (obj.type !== "message" || !obj.message) continue;
- const msg = obj.message;
- const role = msg.role;
- if (!role) continue;
-
- // Text aus content-Array extrahieren
- let text = "";
- if (typeof msg.content === "string") text = msg.content;
- else if (Array.isArray(msg.content)) text = msg.content.filter(c => c.type === "text").map(c => c.text || "").join("\n");
- if (!text) continue;
-
- if (role === "user") {
- // Metadata-Prefix entfernen: "Sender (untrusted metadata):\n```json\n{...}\n```\n\n[timestamp] Text"
- text = text.replace(/^Sender \(untrusted metadata\):[\s\S]*?```[\s\S]*?```\s*\n*/m, "").trim();
- // Timestamp-Prefix entfernen: "[Sat 2026-03-28 14:51 UTC] "
- text = text.replace(/^\[.*?\]\s*/, "").trim();
- chatMessages.push({ type: "sent", text, meta: "Gateway direkt", ts: msg.timestamp || obj.timestamp || 0 });
- } else if (role === "assistant") {
- // Reply-Prefix entfernen: "[[reply_to_current]] "
- text = text.replace(/^\[\[reply_to_\w+\]\]\s*/g, "").trim();
- const ts = msg.timestamp || obj.timestamp || 0;
- // ARIA-File-Marker aus dem Text parsen β pro existierender Datei
- // eine separate file_from_aria-aehnliche Message einfuegen damit die
- // Anhang-Bubble nach Browser-Refresh wieder erscheint.
- const fileRe = /\[FILE:\s*(\/shared\/uploads\/[^\]]+?)\s*\]/gi;
- let m;
- while ((m = fileRe.exec(text)) !== null) {
- const p = m[1].trim();
- try {
- if (fs.existsSync(p)) {
- const st = fs.statSync(p);
- const ext = path.extname(p).toLowerCase();
- const mimeMap = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif",
- ".webp": "image/webp", ".svg": "image/svg+xml", ".pdf": "application/pdf",
- ".mp3": "audio/mpeg", ".mid": "audio/midi", ".midi": "audio/midi",
- ".wav": "audio/wav", ".txt": "text/plain", ".md": "text/markdown",
- ".json": "application/json", ".zip": "application/zip" };
- chatMessages.push({
- type: "aria_file",
- serverPath: p,
- name: path.basename(p),
- mimeType: mimeMap[ext] || "application/octet-stream",
- size: st.size,
- ts,
- });
- }
- } catch {}
- }
- if (text) chatMessages.push({ type: "received", text, meta: "chat:final", ts });
- }
+ if (obj.type === "file_deleted" && obj.path) deletedPaths.add(obj.path);
} catch {}
}
- clientWs.send(JSON.stringify({ type: "chat_history", messages: chatMessages }));
- log("info", "server", `Chat-History geladen: ${chatMessages.length} Nachrichten`);
+ for (const line of lines) {
+ let obj;
+ try { obj = JSON.parse(line); } catch { continue; }
+ if (obj.type === "file_deleted") continue; // Marker selbst nicht anzeigen
+ if (obj.role !== "user" && obj.role !== "assistant") continue;
+ const ts = obj.ts || 0;
+ const text = String(obj.text || "");
+ if (obj.role === "user") {
+ if (text) messages.push({ type: "sent", text, meta: "Gateway direkt", ts });
+ continue;
+ }
+ // assistant: nach FILE-Markern scannen, eigene aria_file-Eintraege pro Datei
+ const fileRe = /\[FILE:\s*(\/shared\/uploads\/[^\]]+?)\s*\]/gi;
+ let m;
+ while ((m = fileRe.exec(text)) !== null) {
+ const p = m[1].trim();
+ const wasDeleted = deletedPaths.has(p);
+ let size = 0;
+ let exists = false;
+ try { const st = fs.statSync(p); size = st.size; exists = true; } catch {}
+ if (!exists && !wasDeleted) continue; // war vielleicht nie da
+ const ext = path.extname(p).toLowerCase();
+ messages.push({
+ type: "aria_file",
+ serverPath: p,
+ name: path.basename(p),
+ mimeType: mimeMap[ext] || "application/octet-stream",
+ size,
+ ts,
+ deleted: wasDeleted || !exists,
+ });
+ }
+ if (text) messages.push({ type: "received", text, meta: "chat:final", ts });
+ }
+
+ clientWs.send(JSON.stringify({ type: "chat_history", messages }));
+ log("info", "server", `Chat-History geladen: ${messages.length} Eintraege (${deletedPaths.size} geloescht)`);
} catch (err) {
- log("error", "server", `Chat-History laden fehlgeschlagen: ${err.message}`);
+ log("error", "server", `Chat-History: ${err.message}`);
clientWs.send(JSON.stringify({ type: "chat_history", messages: [], error: err.message }));
}
}
-// ββ Session aktivieren βββββββββββββββββββββββββββββββββ
-function handleSetActiveSession(clientWs, sessionKey) {
- if (!sessionKey || typeof sessionKey !== "string") {
- clientWs.send(JSON.stringify({ type: "active_session", ok: false, error: "Kein sessionKey" }));
- return;
- }
- activeSessionKey = sessionKey;
- const ok = persistActiveSession(activeSessionKey);
- log("info", "server", `Aktive Session: ${activeSessionKey}${ok ? "" : " (WARN: nicht persistiert!)"}`);
- if (!ok) {
- clientWs.send(JSON.stringify({ type: "active_session", ok: false, sessionKey: activeSessionKey, error: "Persistierung fehlgeschlagen β /data Volume pruefen" }));
- }
- // Allen Clients mitteilen
- for (const c of browserClients) {
- c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
- }
-}
-
-// ββ Session erstellen ββββββββββββββββββββββββββββββββββ
-async function handleCreateSession(clientWs, sessionName) {
- if (!sessionName || typeof sessionName !== "string" || !/^[a-zA-Z0-9_-]+$/.test(sessionName)) {
- clientWs.send(JSON.stringify({ type: "session_created", ok: false, error: "Ungueltiger Name (nur a-z, 0-9, -, _)" }));
- return;
- }
- try {
- // Session wird automatisch erstellt wenn man die erste Nachricht sendet
- activeSessionKey = sessionName;
- persistActiveSession(activeSessionKey);
- log("info", "server", `Neue Session erstellt und aktiviert: ${sessionName}`);
- // Allen Clients mitteilen
- for (const c of browserClients) {
- c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
- }
- clientWs.send(JSON.stringify({ type: "session_created", ok: true, sessionKey: sessionName }));
- } catch (err) {
- clientWs.send(JSON.stringify({ type: "session_created", ok: false, error: err.message }));
- }
-}
-
-// ββ Session neu starten (Container Restart) ββββββββββββ
+// ββ Session neu starten (Container Restart) β toter Code fuer aria-core
async function handleRestartSession(clientWs) {
try {
log("info", "server", "Starte aria-core Container neu (Session Restart)...");
@@ -2327,86 +2158,6 @@ async function handleRestartSession(clientWs) {
}
}
-// ββ Brain Viewer ββββββββββββββββββββββββββββββββββββββββ
-
-async function handleListBrain(clientWs) {
- try {
- log("info", "server", "Lade Brain-Dateien...");
- const raw = await dockerExec("aria-core", `
- for f in /home/node/.openclaw/workspace/memory/*; do
- [ -f "$f" ] || continue
- name=$(basename "$f")
- size=$(du -h "$f" 2>/dev/null | cut -f1)
- lines=$(wc -l < "$f" 2>/dev/null || echo 0)
- modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0)
- # Frontmatter extrahieren (erste 10 Zeilen)
- head10=$(head -10 "$f" 2>/dev/null | tr '\\n' '|')
- echo "FILE:$name|SIZE:$size|LINES:$lines|MODIFIED:$modified|HEAD:$head10"
- done
- `.trim());
-
- const files = [];
- for (const line of raw.split("\n")) {
- if (!line.startsWith("FILE:")) continue;
- const parts = {};
- for (const seg of line.split("|")) {
- const idx = seg.indexOf(":");
- if (idx > 0) {
- const key = seg.slice(0, idx);
- const val = seg.slice(idx + 1);
- // HEAD hat mehrere |, also nur die bekannten Keys parsen
- if (["FILE", "SIZE", "LINES", "MODIFIED"].includes(key)) {
- parts[key] = val;
- }
- }
- }
- if (!parts.FILE || parts.FILE === "*") continue;
-
- // Frontmatter-Info aus HEAD extrahieren
- let description = "";
- let memType = "";
- const headPart = line.slice(line.indexOf("|HEAD:") + 6);
- if (headPart) {
- const headLines = headPart.split("|");
- for (const hl of headLines) {
- if (hl.startsWith("description:")) description = hl.replace("description:", "").trim();
- if (hl.startsWith("type:")) memType = hl.replace("type:", "").trim();
- }
- }
-
- files.push({
- name: parts.FILE,
- size: parts.SIZE || "?",
- lines: parseInt(parts.LINES) || 0,
- modified: parseInt(parts.MODIFIED) || 0,
- description,
- memType,
- });
- }
-
- files.sort((a, b) => b.modified - a.modified);
- clientWs.send(JSON.stringify({ type: "brain_list", files }));
- log("info", "server", `${files.length} Brain-Datei(en) gefunden`);
- } catch (err) {
- log("error", "server", `Brain laden fehlgeschlagen: ${err.message}`);
- clientWs.send(JSON.stringify({ type: "brain_list", files: [], error: err.message }));
- }
-}
-
-async function handleReadBrainFile(clientWs, filename) {
- // Path Traversal verhindern
- if (!filename || filename.includes("..") || filename.includes("/")) {
- clientWs.send(JSON.stringify({ type: "brain_content", error: "Ungueltiger Dateiname" }));
- return;
- }
- try {
- const content = await dockerExec("aria-core",
- `cat '/home/node/.openclaw/workspace/memory/${filename.replace(/'/g, "")}'`);
- clientWs.send(JSON.stringify({ type: "brain_content", filename, content }));
- } catch (err) {
- clientWs.send(JSON.stringify({ type: "brain_content", filename, error: err.message }));
- }
-}
// ββ Einstellungen: Tool-Berechtigungen ββββββββββββββββββ
// ENTFERNT: Granulare Permissions haben nie funktioniert.
@@ -2451,45 +2202,22 @@ async function handleSetModel(clientWs, model) {
}
}
-// ββ Einstellungen: OpenClaw Config ββββββββββββββββββββββ
-
-async function handleGetOpenClawConfig(clientWs) {
- try {
- const raw = await dockerExec("aria-core", `
- echo '=== Umgebungsvariablen ==='
- echo "DEFAULT_MODEL=$DEFAULT_MODEL"
- echo "RATE_LIMIT_PER_USER=$RATE_LIMIT_PER_USER"
- echo "OPENCLAW_GATEWAY_TOKEN=$(echo $OPENCLAW_GATEWAY_TOKEN | head -c 8)..."
- echo "OPENCLAW_GATEWAY_BIND=$OPENCLAW_GATEWAY_BIND"
- echo ""
- echo '=== openclaw.json ==='
- cat /home/node/.openclaw/openclaw.json 2>/dev/null || echo "(nicht vorhanden)"
- echo ""
- echo '=== exec-approvals.json ==='
- cat /home/node/.openclaw/exec-approvals.json 2>/dev/null || echo "(nicht vorhanden)"
- echo ""
- echo '=== Agent-Verzeichnis ==='
- ls -la /home/node/.openclaw/agents/main/agent/ 2>&1
- echo ""
- echo '=== Workspace ==='
- ls -la /home/node/.openclaw/workspace/ 2>&1
- `.trim());
- clientWs.send(JSON.stringify({ type: "openclaw_config", config: raw }));
- } catch (err) {
- clientWs.send(JSON.stringify({ type: "openclaw_config", error: err.message }));
- }
-}
+// OpenClaw-Config-Handler entfernt β aria-core ist raus.
// ββ Start βββββββββββββββββββββββββββββββββββββββββββββββ
server.listen(HTTP_PORT, "0.0.0.0", () => {
log("info", "server", `Diagnostic Server laeuft auf http://0.0.0.0:${HTTP_PORT}`);
- log("info", "server", `Gateway: ${GATEWAY_URL}`);
- log("info", "server", `Token: ${GATEWAY_TOKEN ? GATEWAY_TOKEN.slice(0, 8) + "..." : "(keiner)"}`);
log("info", "server", `RVS: ${RVS_HOST ? `${RVS_HOST}:${RVS_PORT}` : "(nicht konfiguriert)"}`);
log("info", "server", `Proxy: ${PROXY_URL}`);
+ log("info", "server", `Brain: ${process.env.BRAIN_URL || "http://aria-brain:8080"}`);
- // Verbindungen aufbauen
- connectGateway();
+ // RVS-Verbindung β aria-core/OpenClaw-Gateway entfaellt.
connectRVS();
+
+ // gateway-State bleibt "disconnected" stehen; State-Card im UI
+ // wurde mit ausgebaut.
+ state.gateway.status = "disabled";
+ state.gateway.lastError = "aria-core entfernt β Brain-Loop in Arbeit";
+ broadcastState();
});