fix(diagnostic): chat_history-Render verträgt kaputte Bubbles + EHOSTUNREACH skipped TLS-Fallback

Zwei kleine Robustness-Verbesserungen:

1) chat_history-Handler im Frontend: jede Bubble jetzt in try/catch. Wenn
   eine Bubble bei der Render-Pipeline (escape/linkify/regex-replace) eine
   Exception wirft, brach die ganze for-Schleife ab und alle nachfolgenden
   Bubbles wurden nicht mehr in den DOM geschrieben — beim Reload sah man
   dann nur die ersten N Eintraege und Stefan dachte die letzten Antworten
   waeren weg. Jetzt: Fehler-Bubble mit "⚠ Render-Fehler" + console.error,
   restliche Bubbles laufen weiter durch.

2) Diagnostic-Server RVS-Reconnect: TLS-Fallback war auch bei reinen
   Netz-Fehlern (EHOSTUNREACH, ECONNREFUSED, ENETUNREACH, ETIMEDOUT,
   ENOTFOUND, EAI_AGAIN) gefeuert — bringt nichts weil der Server eh tot
   ist, generiert aber doppelte Reconnect-Versuche + Log-Spam. Jetzt nur
   noch bei wirklichen TLS/Handshake-Fehlern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 10:28:57 +02:00
parent ad87c807de
commit b94626787b
2 changed files with 53 additions and 27 deletions
+43 -25
View File
@@ -1650,36 +1650,54 @@
if (msg.type === 'chat_history') {
const boxes = [chatBox, document.getElementById('chat-box-fs')].filter(Boolean);
for (const b of boxes) b.innerHTML = '';
let errorCount = 0;
if (msg.messages && msg.messages.length > 0) {
for (const m of msg.messages) {
if (m.type === 'aria_file') {
// 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;
}
// [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
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'">`;
});
const time = m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?';
const trashBtn = m.ts
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${m.ts})">🗑</button>`
: '';
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);
for (let mi = 0; mi < msg.messages.length; mi++) {
const m = msg.messages[mi];
try {
if (m.type === 'aria_file') {
addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size, deleted: m.deleted });
continue;
}
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);
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'">`;
});
const time = m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?';
const trashBtn = m.ts
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${m.ts})">🗑</button>`
: '';
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);
}
} catch (renderErr) {
// Eine kaputte Bubble darf nicht den Rest der History killen.
// Vorher passierte genau das: Frontend-Render bracht bei einer
// problematischen Antwort ab, alle nachfolgenden Nachrichten waren
// beim Reload weg. Jetzt: Fehler-Bubble einbauen + weitermachen.
errorCount++;
console.error('chat_history render error at idx ' + mi + ':', renderErr, m);
for (const b of boxes) {
const el = document.createElement('div');
el.className = `chat-msg ${m.type || 'received'}`;
if (m.ts) el.dataset.ts = String(m.ts);
el.innerHTML = `<span style="color:#FF6B6B;">⚠ Render-Fehler in Bubble (${escapeHtml(String(renderErr.message || renderErr))})</span><div class="meta">${m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?'}</div>`;
b.appendChild(el);
}
}
}
for (const b of boxes) b.scrollTop = b.scrollHeight;
}
if (errorCount > 0) {
console.warn(`chat_history: ${errorCount} Bubble(s) konnten nicht gerendert werden`);
}
return;
}
+10 -2
View File
@@ -701,8 +701,16 @@ function connectRVS(forcePlain) {
state.rvs.lastError = err.message;
broadcastState();
// TLS Fallback
if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered) {
// TLS-Fallback nur bei wirklichen TLS/Handshake-Fehlern.
// Bei Netz-Problemen wie EHOSTUNREACH, ECONNREFUSED, ENETUNREACH,
// EAI_AGAIN ist der Server eh tot — Fallback bringt nichts ausser
// Log-Spam und doppelten Retries.
const netErr = (err.code || err.message || "").toString();
const isNetDown =
/^(EHOSTUNREACH|ECONNREFUSED|ENETUNREACH|ETIMEDOUT|EAI_AGAIN|ENOTFOUND)$/.test(netErr) ||
/EHOSTUNREACH|ECONNREFUSED|ENETUNREACH|ETIMEDOUT|EAI_AGAIN|ENOTFOUND/.test(err.message || "");
if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered && !isNetDown) {
fallbackTriggered = true;
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
try { ws.removeAllListeners(); ws.close(); } catch (_) {}