fix(bridge): 3-Schichten-Schutz gegen Bridge-Hangs + Chat-History in beide Boxen
Bridge hat seit 5+h still gehangen — Container Up, asyncio idle im selectors.select(), TCP-Verbindung zum RVS ESTABLISHED, aber keine Events mehr verarbeitet. Klassischer Fall: NAT-Tabelle/Firewall hat die TCP-Verbindung still gekillt (kein RST), Linux-Kernel mit Default- Keepalive (2h idle) hat's nicht gemerkt, und der ws.ping()-Future hat im Limbo gehangen ohne Exception zu werfen. Schicht 1 — TCP-Keepalive aufm Socket: SO_KEEPALIVE=1, TCP_KEEPIDLE=30s, TCP_KEEPINTVL=10s, TCP_KEEPCNT=3. Halb-tote Verbindungen werden in ~1 min mit ECONNRESET sichtbar statt nach 2h. Loest 80% der Faelle direkt. Schicht 2 — Asyncio-Watchdog (_rvs_heartbeat_watchdog): Separate Coroutine parallel zu _rvs_heartbeat. Letzterer markiert _last_heartbeat_ok nach jedem erfolgreichen pong. Watchdog checkt alle 20s: > 60s stale → ws.close() + transport.close() als Notausgang. Schuetzt gegen ws.ping()-Limbo. Schicht 3 — File-Based Liveness Thread: Separater OS-Thread (NICHT asyncio) — immun gegen asyncio-Hangs. Schreibt /shared/health/bridge_alive periodisch. Wenn _last_heartbeat_ok > 180s stale: os._exit(1), Docker restart_policy uebernimmt. Last-Resort wenn Schichten 1+2 versagen. Plus: chat_history-Render nach Reload bezog nur #chat-box, nicht #chat-box-fs (Vollbild). Wer im FS-Modus reloaded hat sah eine leere Box statt der History. Jetzt rendert der Handler in beide Boxen (gleicher Pattern wie addChat / addAriaFile). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-12
@@ -1642,26 +1642,26 @@
|
||||
showDockerLogs(msg);
|
||||
return;
|
||||
}
|
||||
// Chat-History (nach F5 / Reconnect)
|
||||
// Chat-History (nach F5 / Reconnect) — IN BEIDE Boxen rendern.
|
||||
// Vorher: nur chatBox bekam die Replay, die Vollbild-Box blieb leer
|
||||
// → bei Reload aus dem FS-Modus sah es so aus als ob die letzten
|
||||
// Bubbles weg waeren. Live-addChat schreibt schon korrekt in beide,
|
||||
// der Reload-Pfad zog nicht mit.
|
||||
if (msg.type === 'chat_history') {
|
||||
chatBox.innerHTML = '';
|
||||
const boxes = [chatBox, document.getElementById('chat-box-fs')].filter(Boolean);
|
||||
for (const b of boxes) b.innerHTML = '';
|
||||
if (msg.messages && msg.messages.length > 0) {
|
||||
for (const m of msg.messages) {
|
||||
if (m.type === 'aria_file') {
|
||||
// ARIA-Datei-Bubble rekonstruieren (statt addAriaFile damit
|
||||
// kein Auto-Scroll-Race waehrend des Bulk-Loads)
|
||||
addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size });
|
||||
// ARIA-Datei-Bubble — addAriaFile schreibt selbst in beide Boxen
|
||||
addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size, deleted: m.deleted });
|
||||
continue;
|
||||
}
|
||||
const el = document.createElement('div');
|
||||
el.className = `chat-msg ${m.type}`;
|
||||
if (m.ts) el.dataset.ts = String(m.ts);
|
||||
// [FILE: ...]-Marker rausfiltern (gleicher Filter wie addChat)
|
||||
const cleaned = (m.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
|
||||
const escaped = escapeHtml(cleaned);
|
||||
let linked = linkifyText(escaped);
|
||||
// /shared/uploads/-Bildpfade auch im History inline rendern
|
||||
// (gleicher Replace wie in addChat — sonst sieht man nach F5 nur Text-Pfade)
|
||||
linked = linked.replace(/\/shared\/uploads\/[^\s<"]+\.(jpg|jpeg|png|gif|webp|svg|bmp)/gi, (match) => {
|
||||
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
|
||||
});
|
||||
@@ -1669,10 +1669,16 @@
|
||||
const trashBtn = m.ts
|
||||
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${m.ts})">🗑</button>`
|
||||
: '';
|
||||
el.innerHTML = `${trashBtn}${linked}<div class="meta">${escapeHtml(m.meta)} — ${time}</div>`;
|
||||
chatBox.appendChild(el);
|
||||
const innerHtml = `${trashBtn}${linked}<div class="meta">${escapeHtml(m.meta)} — ${time}</div>`;
|
||||
for (const b of boxes) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `chat-msg ${m.type}`;
|
||||
if (m.ts) el.dataset.ts = String(m.ts);
|
||||
el.innerHTML = innerHtml;
|
||||
b.appendChild(el);
|
||||
}
|
||||
}
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
for (const b of boxes) b.scrollTop = b.scrollHeight;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user