fix(projects): Live-Sync verstecken zwischen App und Diagnostic (ohne Refresh)

Kern-Bug: der Diagnostic-Server reichte ein von RVS empfangenes
project_changed NIE an die Browser weiter — der Handler in index.html war
toter Code, der Diagnostic aktualisierte die Projektliste also nie live.

- server.js: RVS-Handler forwardet project_changed jetzt an die Browser-Tabs;
  der /api/brain-Proxy broadcastet project_changed (RVS + lokal) nach jeder
  erfolgreichen Projekt-Mutation (create/switch/end/archive/PATCH inkl. hidden).
- App ProjectsBrowser: sendet project_changed nach dem Verstecken/Sichtbar-
  machen und laedt bei eingehendem project_changed selbst neu.

Damit: verstecken in der App -> sofort im Diagnostic weg (und umgekehrt),
ohne Seiten-Refresh. Diagnostic-Hide funktionierte schon (Daten/Proxy/Logik
korrekt) — versteckte sind per "Versteckte anzeigen (N)"-Toggle sichtbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 17:11:51 +02:00
co-authored by Claude Opus 4.8
parent 9ec9119bd9
commit fed3cb9f18
2 changed files with 43 additions and 2 deletions
+20 -2
View File
@@ -112,6 +112,17 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
return () => unsub(); return () => unsub();
}, [visible, load]); }, [visible, load]);
// Live-Sync: ein anderer Client (Diagnostic / andere App) hat ein Projekt
// geaendert (verstecken/anlegen/beenden/…) → project_changed ueber RVS →
// Liste neu laden, ohne dass Stefan manuell refreshen muss.
useEffect(() => {
if (!visible) return;
const unsub = rvs.onMessage((msg: any) => {
if (msg?.type === 'project_changed') load();
});
return () => unsub();
}, [visible, load]);
const switchTo = useCallback((id: string) => { const switchTo = useCallback((id: string) => {
// Multi-Threading: Focus-Wechsel ist reine App-lokale UI-Entscheidung. // Multi-Threading: Focus-Wechsel ist reine App-lokale UI-Entscheidung.
// Brain wird nicht mehr benachrichtigt (kein globaler active_project mehr). // Brain wird nicht mehr benachrichtigt (kein globaler active_project mehr).
@@ -161,11 +172,18 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
]); ]);
}, [load]); }, [load]);
// Nach einer Projekt-Mutation die anderen Clients (Diagnostic, weitere
// App-Instanzen) live aktualisieren — via RVS project_changed. RVS echot
// NICHT an den Sender zurueck, darum laden wir lokal zusaetzlich selbst.
const broadcastProjectsChanged = useCallback(() => {
try { rvs.send('project_changed' as any, { reason: 'app' }); } catch {}
}, []);
const toggleHidden = useCallback((p: Project) => { const toggleHidden = useCallback((p: Project) => {
brainApi.setProjectHidden(p.id, !p.hidden) brainApi.setProjectHidden(p.id, !p.hidden)
.then(() => load()) .then(() => { broadcastProjectsChanged(); load(); })
.catch(e => Alert.alert('Fehler', String(e?.message || e))); .catch(e => Alert.alert('Fehler', String(e?.message || e)));
}, [load]); }, [load, broadcastProjectsChanged]);
const archiveProject = useCallback((p: Project) => { const archiveProject = useCallback((p: Project) => {
Alert.alert(`"${p.name}" archivieren?`, Alert.alert(`"${p.name}" archivieren?`,
+23
View File
@@ -898,6 +898,11 @@ function connectRVS(forcePlain) {
// Mode-Broadcast von der Bridge → an Browser-Clients weiterreichen // Mode-Broadcast von der Bridge → an Browser-Clients weiterreichen
log("info", "rvs", `Mode-Broadcast: ${msg.payload?.mode} (${msg.payload?.name})`); log("info", "rvs", `Mode-Broadcast: ${msg.payload?.mode} (${msg.payload?.name})`);
broadcast({ type: "mode", payload: msg.payload }); broadcast({ type: "mode", payload: msg.payload });
} else if (msg.type === "project_changed") {
// Ein Projekt wurde geaendert (ARIA-Tool, App-Verstecken, …) → an die
// Browser-Tabs weiterreichen, damit die Projektliste live neu laedt
// (bisher wurde das NICHT geforwardet → Diagnostic aktualisierte nie).
broadcast({ type: "project_changed", payload: msg.payload || {} });
} else if (msg.type === "agent_activity") { } else if (msg.type === "agent_activity") {
// Bridge meldet "ARIA denkt/schreibt/tool" oder "idle" — an Browser // Bridge meldet "ARIA denkt/schreibt/tool" oder "idle" — an Browser
// weiterreichen, damit der Thinking-Indikator im Chat erscheint. // weiterreichen, damit der Thinking-Indikator im Chat erscheint.
@@ -2168,6 +2173,13 @@ const server = http.createServer((req, res) => {
// mehr als eine Minute. // mehr als eine Minute.
const isUpload = /\/attachments(\/upload)?$/.test(targetPath); const isUpload = /\/attachments(\/upload)?$/.test(targetPath);
const timeout = isUpload ? 120000 : 60000; const timeout = isUpload ? 120000 : 60000;
// Projekt-Mutationen (create/switch/end/archive/patch inkl. hidden) sollen
// alle Clients live aktualisieren. Wir broadcasten nach Erfolg ein
// project_changed an RVS — App + andere Diagnostic-Tabs laden dann neu,
// ohne Seiten-Refresh (spiegelt das bestehende ARIA-project_changed-Event).
const isProjectMutation =
/^\/projects\b/.test(targetPath) &&
(req.method === "POST" || req.method === "PATCH" || req.method === "DELETE");
const proxyReq = http.request({ const proxyReq = http.request({
host: "aria-brain", host: "aria-brain",
port: 8080, port: 8080,
@@ -2178,6 +2190,17 @@ const server = http.createServer((req, res) => {
}, (proxyRes) => { }, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers); res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res); proxyRes.pipe(res);
if (isProjectMutation && proxyRes.statusCode >= 200 && proxyRes.statusCode < 300) {
try {
// An App + Bridge (RVS echot NICHT an den Sender) …
sendToRVS_raw({ type: "project_changed",
payload: { reason: "diagnostic" },
timestamp: Date.now() });
// … und an die eigenen Browser-Tabs (die haengen am Diag-Server, nicht
// direkt am RVS, kriegen den RVS-Broadcast also nicht).
broadcast({ type: "project_changed", payload: { reason: "diagnostic" } });
} catch (_) {}
}
}); });
proxyReq.on("error", (err) => { proxyReq.on("error", (err) => {
res.writeHead(503, { "Content-Type": "application/json" }); res.writeHead(503, { "Content-Type": "application/json" });