Compare commits
4 Commits
v0.1.6.2
...
474e2c6c50
| Author | SHA1 | Date | |
|---|---|---|---|
| 474e2c6c50 | |||
| 3e0cfef63c | |||
| b94626787b | |||
| ad87c807de |
+19
-1
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { PermissionsAndroid, Platform, StatusBar, StyleSheet } from 'react-native';
|
import { AppState, AppStateStatus, PermissionsAndroid, Platform, StatusBar, StyleSheet } from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
|
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
@@ -107,8 +107,26 @@ const App: React.FC = () => {
|
|||||||
console.warn('[App] GPS-Tracking restore fehlgeschlagen:', err?.message || err);
|
console.warn('[App] GPS-Tracking restore fehlgeschlagen:', err?.message || err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// AppState-Listener: nach Hintergrund-Rueckkehr aktiv die WS-
|
||||||
|
// Verbindung neu aufbauen. Hintergrund: Android kann den TCP-Socket
|
||||||
|
// im Background killen, JS-State zeigt aber noch OPEN → Stefan musste
|
||||||
|
// manuell in Settings auf "Verbinden" tippen, oft mehrfach. Mit dem
|
||||||
|
// force-Reconnect bei "active" greift das automatisch.
|
||||||
|
let lastAppState: AppStateStatus = AppState.currentState;
|
||||||
|
const appStateSub = AppState.addEventListener('change', (next) => {
|
||||||
|
const wasBg = lastAppState !== 'active';
|
||||||
|
lastAppState = next;
|
||||||
|
if (next === 'active' && wasBg) {
|
||||||
|
console.log('[App] Foreground-Resume — force-reconnect zum RVS');
|
||||||
|
try { rvs.connect(true); } catch (e: any) {
|
||||||
|
console.warn('[App] force-reconnect fehlgeschlagen:', e?.message || e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Beim Beenden: Verbindung sauber trennen
|
// Beim Beenden: Verbindung sauber trennen
|
||||||
return () => {
|
return () => {
|
||||||
|
appStateSub.remove();
|
||||||
rvs.disconnect();
|
rvs.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -83,21 +83,39 @@ class RVSConnection {
|
|||||||
|
|
||||||
// --- Verbindung ---
|
// --- Verbindung ---
|
||||||
|
|
||||||
/** Verbindung zum RVS aufbauen */
|
/** Verbindung zum RVS aufbauen. force=true: bestehende Connection hart
|
||||||
connect(): void {
|
* schliessen + neu verbinden (auch wenn JS denkt readyState=OPEN — kann
|
||||||
|
* nach Hintergrund-Pause ein Zombie-WS sein wo TCP tot ist aber JS-State
|
||||||
|
* noch OPEN zeigt; in dem Fall war "Bereits verbunden" ein No-Op und
|
||||||
|
* Stefan musste manuell zigmal klicken). */
|
||||||
|
connect(force: boolean = false): void {
|
||||||
if (!this.config) {
|
if (!this.config) {
|
||||||
this.log('warn', 'Keine Verbindungskonfiguration vorhanden');
|
this.log('warn', 'Keine Verbindungskonfiguration vorhanden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
if (!force && this.ws?.readyState === WebSocket.OPEN) {
|
||||||
this.log('info', 'Bereits verbunden');
|
this.log('info', 'Bereits verbunden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wenn ein WS-Objekt da ist (Zombie oder lebend), sauber abreissen
|
||||||
|
// bevor wir einen neuen aufbauen — sonst gibt's zwei parallele
|
||||||
|
// Verbindungen + doppelte Events.
|
||||||
|
if (this.ws) {
|
||||||
|
this.log('info', 'Bestehende WS-Verbindung wird geschlossen vor Neu-Connect');
|
||||||
|
try {
|
||||||
|
this.ws.onclose = null; // verhindert dass scheduleReconnect doppelt feuert
|
||||||
|
this.ws.onerror = null;
|
||||||
|
this.ws.close();
|
||||||
|
} catch (_) {}
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.shouldReconnect = true;
|
this.shouldReconnect = true;
|
||||||
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
||||||
this.usingTLSFallback = false;
|
this.usingTLSFallback = false;
|
||||||
|
this.clearTimers();
|
||||||
this.log('info', `Verbindungsaufbau zu ${this.config.host}:${this.config.port} (TLS: ${this.config.useTLS ? 'ja' : 'nein'})`);
|
this.log('info', `Verbindungsaufbau zu ${this.config.host}:${this.config.port} (TLS: ${this.config.useTLS ? 'ja' : 'nein'})`);
|
||||||
this.establishConnection();
|
this.establishConnection();
|
||||||
}
|
}
|
||||||
@@ -212,6 +230,16 @@ class RVSConnection {
|
|||||||
this.ws = null;
|
this.ws = null;
|
||||||
this.setState('disconnected');
|
this.setState('disconnected');
|
||||||
|
|
||||||
|
// Sticky-Fallback-Reset: beim naechsten Reconnect wieder primary
|
||||||
|
// (wss://) versuchen statt fuer immer auf ws:// zu kleben. War
|
||||||
|
// der Hauptgrund warum die App nach Hintergrund-Rueckkehr nicht
|
||||||
|
// mehr verband — TLS-Handshake-Timeout in einem Reconnect → Fallback
|
||||||
|
// auf ws:// → Caddy refused → endlos im Fallback haengen.
|
||||||
|
if (this.usingTLSFallback) {
|
||||||
|
this.log('info', 'Reset TLS-Fallback fuer naechsten Reconnect (zurueck zu wss://)');
|
||||||
|
this.usingTLSFallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.shouldReconnect) {
|
if (this.shouldReconnect) {
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,18 @@ META_TOOLS = [
|
|||||||
"Erstelle einen neuen Skill (wiederverwendbare Faehigkeit). "
|
"Erstelle einen neuen Skill (wiederverwendbare Faehigkeit). "
|
||||||
"Skills sind IMMER Python — jeder Skill bekommt seine eigene venv "
|
"Skills sind IMMER Python — jeder Skill bekommt seine eigene venv "
|
||||||
"mit den pip_packages die er braucht.\n\n"
|
"mit den pip_packages die er braucht.\n\n"
|
||||||
|
"PFLICHT VORHER:\n"
|
||||||
|
" - `skill_list` aufrufen und pruefen ob ein passender Skill schon "
|
||||||
|
"existiert. Wenn ja: `skill_update` statt neu anlegen.\n"
|
||||||
|
" - Name OHNE Versionssuffix waehlen (kein `-v2`, `_v3`, `-new`, "
|
||||||
|
"`-fixed`, `-aria`, `-ctl`). Versionsverwaltung ist intern, Du brauchst "
|
||||||
|
"nur einen klaren Namen.\n"
|
||||||
|
" - Bei OAuth-Services (Spotify, Google, GitHub etc.): NIEMALS "
|
||||||
|
"client_id/client_secret/Tokens in den Code schreiben. Nutze "
|
||||||
|
"`oauth_get_token('<service>')` — das macht Auto-Refresh. Sonst muss "
|
||||||
|
"Stefan sich alle 60min manuell neu einloggen.\n"
|
||||||
|
" - Bei konfigurierbaren Werten (User-IDs, Endpoints, Defaults): "
|
||||||
|
"ueber `config_schema` deklarieren, NICHT hardcoden.\n\n"
|
||||||
"HARTE REGEL — IMMER Skill anlegen wenn: die Loesung erfordert eine "
|
"HARTE REGEL — IMMER Skill anlegen wenn: die Loesung erfordert eine "
|
||||||
"pip-Library. Sonst muesste der Install bei jedem Container-Restart "
|
"pip-Library. Sonst muesste der Install bei jedem Container-Restart "
|
||||||
"neu laufen (Brain hat keinen persistenten State ausser /data/skills/).\n\n"
|
"neu laufen (Brain hat keinen persistenten State ausser /data/skills/).\n\n"
|
||||||
|
|||||||
+8
-1
@@ -37,6 +37,7 @@ import triggers as triggers_mod
|
|||||||
import watcher as watcher_mod
|
import watcher as watcher_mod
|
||||||
import background as background_mod
|
import background as background_mod
|
||||||
import oauth as oauth_mod
|
import oauth as oauth_mod
|
||||||
|
import seed_rules as seed_rules_mod
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
||||||
logger = logging.getLogger("aria-brain")
|
logger = logging.getLogger("aria-brain")
|
||||||
@@ -46,7 +47,13 @@ QDRANT_PORT = int(os.environ.get("QDRANT_PORT", "6333"))
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""Beim Brain-Start: Trigger-Background-Loop anwerfen. Beim Shutdown: stoppen."""
|
"""Beim Brain-Start: System-Seed-Regeln idempotent in DB schreiben,
|
||||||
|
Trigger-Background-Loop anwerfen. Beim Shutdown: Loop stoppen."""
|
||||||
|
try:
|
||||||
|
result = seed_rules_mod.apply(store(), embedder())
|
||||||
|
logger.info("Lifespan: seed_rules angewendet (%s)", result)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Lifespan: seed_rules fehlgeschlagen — Brain startet trotzdem (%s)", exc)
|
||||||
task = asyncio.create_task(background_mod.run_loop(agent))
|
task = asyncio.create_task(background_mod.run_loop(agent))
|
||||||
logger.info("Lifespan: Trigger-Loop gestartet")
|
logger.info("Lifespan: Trigger-Loop gestartet")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
"""
|
||||||
|
System-Seed-Regeln — werden bei jedem Brain-Boot idempotent in die
|
||||||
|
Vector-DB geschrieben (pinned, source="seed").
|
||||||
|
|
||||||
|
Im Gegensatz zu aria-data/brain-import/ (User-Saatgut, manuell via
|
||||||
|
Diagnostic-Klick migriert) ist das hier System-Regeln, die zum Brain-Code
|
||||||
|
gehoeren und mit jedem Deploy ausgerollt werden.
|
||||||
|
|
||||||
|
Idempotenz: Punkte mit gleicher `migration_key` werden vor dem Schreiben
|
||||||
|
geloescht. Editieren = Zeile aendern, Brain neu starten, fertig.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from memory import Embedder, VectorStore
|
||||||
|
from memory.vector_store import COLLECTION
|
||||||
|
from qdrant_client.http import models as qm
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Jede Regel = ein eigener Memory-Punkt. Klein halten, klar formulieren —
|
||||||
|
# ARIA sieht das in jedem Chat-Turn als pinned Hot Memory.
|
||||||
|
SEED_RULES: List[dict] = [
|
||||||
|
{
|
||||||
|
"migration_key": "seed/skill-rule/list-before-create",
|
||||||
|
"type": "rule",
|
||||||
|
"title": "Skill-Regel: skill_list vor skill_create",
|
||||||
|
"category": "skills",
|
||||||
|
"content": (
|
||||||
|
"Bevor du einen neuen Skill mit `skill_create` anlegst, ruf IMMER "
|
||||||
|
"zuerst `skill_list` auf. Schau dir die Namen und Descriptions an. "
|
||||||
|
"Wenn ein passender Skill existiert: verwende ihn oder verbessere "
|
||||||
|
"ihn mit `skill_update`. Lege keinen Duplikat-Skill an."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"migration_key": "seed/skill-rule/no-version-suffix",
|
||||||
|
"type": "rule",
|
||||||
|
"title": "Skill-Regel: keine Versions-Suffixe im Namen",
|
||||||
|
"category": "skills",
|
||||||
|
"content": (
|
||||||
|
"Skill-Namen muessen permanent und beschreibend sein. NIEMALS "
|
||||||
|
"Suffixe wie `-v2`, `_v3`, `-new`, `-fixed`, `-aria`, `-ctl` "
|
||||||
|
"anhaengen, um eine neue Variante zu bauen. Wenn ein Skill kaputt "
|
||||||
|
"ist oder verbessert werden soll: `skill_update`. Versionsverwaltung "
|
||||||
|
"macht das System intern (Rollback ueber `skill_rollback`)."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"migration_key": "seed/skill-rule/update-not-recreate",
|
||||||
|
"type": "rule",
|
||||||
|
"title": "Skill-Regel: kaputten Skill reparieren, nicht neu bauen",
|
||||||
|
"category": "skills",
|
||||||
|
"content": (
|
||||||
|
"Wenn ein vorhandener Skill nicht wie erwartet funktioniert, lies "
|
||||||
|
"zuerst Code + Logs (`skill_get`, `skill_logs`). Repariere ihn dann "
|
||||||
|
"mit `skill_update` (entry_code, readme oder pip_packages patchen). "
|
||||||
|
"Baue NIEMALS einen zweiten Skill mit aehnlichem Namen — das gibt "
|
||||||
|
"Skill-Friedhof und Stefan muss aufraeumen."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"migration_key": "seed/skill-rule/no-hardcoded-credentials",
|
||||||
|
"type": "rule",
|
||||||
|
"title": "Skill-Regel: keine hardcoded Credentials",
|
||||||
|
"category": "skills",
|
||||||
|
"content": (
|
||||||
|
"Schreibe NIEMALS API-Keys, Tokens, Passwoerter, client_id oder "
|
||||||
|
"client_secret direkt in den Skill-Code. Fuer OAuth-Services "
|
||||||
|
"(Spotify, Google, GitHub etc.) nutze das Brain-Tool "
|
||||||
|
"`oauth_get_token('<service>')` — das macht Auto-Refresh und "
|
||||||
|
"haelt den Token frisch. Stefan muss sich sonst alle 60 Minuten "
|
||||||
|
"manuell neu einloggen, das nervt."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"migration_key": "seed/skill-rule/config-schema-for-settings",
|
||||||
|
"type": "rule",
|
||||||
|
"title": "Skill-Regel: konfigurierbare Werte ueber config_schema",
|
||||||
|
"category": "skills",
|
||||||
|
"content": (
|
||||||
|
"Wenn dein Skill konfigurierbare Werte braucht (User-IDs, "
|
||||||
|
"Default-Geraete, Endpoints, nicht-OAuth-API-Keys), deklariere "
|
||||||
|
"sie im `config_schema`-Feld der skill.json. Stefan setzt sie "
|
||||||
|
"dann in der Diagnostic-UI; der Skill bekommt die Werte zur "
|
||||||
|
"Laufzeit als Environment-Variable `CFG_<NAME>`. NICHT als "
|
||||||
|
"Argument, NICHT hardcoded."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def apply(store: VectorStore, embedder: Embedder) -> dict:
|
||||||
|
"""Schreibt alle SEED_RULES idempotent in die DB.
|
||||||
|
|
||||||
|
Vorgehen: erst alle Punkte mit `source=seed` UND passender migration_key
|
||||||
|
loeschen, dann frisch upserten. So koennen Regeln editiert/entfernt
|
||||||
|
werden indem die SEED_RULES-Liste angepasst wird.
|
||||||
|
"""
|
||||||
|
if not SEED_RULES:
|
||||||
|
return {"written": 0}
|
||||||
|
|
||||||
|
migration_keys = [r["migration_key"] for r in SEED_RULES]
|
||||||
|
|
||||||
|
# Alte Versionen entfernen (nur die mit unserer migration_key — andere
|
||||||
|
# source=seed Punkte aus zukuenftigen seed-Files sind sicher)
|
||||||
|
try:
|
||||||
|
store.client.delete(
|
||||||
|
collection_name=COLLECTION,
|
||||||
|
points_selector=qm.FilterSelector(filter=qm.Filter(must=[
|
||||||
|
qm.FieldCondition(key="migration_key", match=qm.MatchAny(any=migration_keys))
|
||||||
|
])),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("seed_rules: delete-by-migration_key fehlgeschlagen (%s) — wahrscheinlich erster Run", exc)
|
||||||
|
|
||||||
|
# Frisch einbetten + schreiben
|
||||||
|
texts = [r["content"] for r in SEED_RULES]
|
||||||
|
vectors = embedder.embed_batch(texts)
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
written = 0
|
||||||
|
for rule, vec in zip(SEED_RULES, vectors):
|
||||||
|
payload = {
|
||||||
|
"type": rule["type"],
|
||||||
|
"title": rule["title"],
|
||||||
|
"content": rule["content"],
|
||||||
|
"pinned": True,
|
||||||
|
"category": rule.get("category", ""),
|
||||||
|
"source": "seed",
|
||||||
|
"tags": [],
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
"migration_key": rule["migration_key"],
|
||||||
|
"attachments": [],
|
||||||
|
}
|
||||||
|
store.client.upsert(
|
||||||
|
collection_name=COLLECTION,
|
||||||
|
points=[qm.PointStruct(id=str(uuid.uuid4()), vector=vec, payload=payload)],
|
||||||
|
)
|
||||||
|
written += 1
|
||||||
|
|
||||||
|
logger.info("seed_rules: %d Regeln in DB geschrieben", written)
|
||||||
|
return {"written": written, "keys": migration_keys}
|
||||||
+43
-25
@@ -1650,36 +1650,54 @@
|
|||||||
if (msg.type === 'chat_history') {
|
if (msg.type === 'chat_history') {
|
||||||
const boxes = [chatBox, document.getElementById('chat-box-fs')].filter(Boolean);
|
const boxes = [chatBox, document.getElementById('chat-box-fs')].filter(Boolean);
|
||||||
for (const b of boxes) b.innerHTML = '';
|
for (const b of boxes) b.innerHTML = '';
|
||||||
|
let errorCount = 0;
|
||||||
if (msg.messages && msg.messages.length > 0) {
|
if (msg.messages && msg.messages.length > 0) {
|
||||||
for (const m of msg.messages) {
|
for (let mi = 0; mi < msg.messages.length; mi++) {
|
||||||
if (m.type === 'aria_file') {
|
const m = msg.messages[mi];
|
||||||
// ARIA-Datei-Bubble — addAriaFile schreibt selbst in beide Boxen
|
try {
|
||||||
addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size, deleted: m.deleted });
|
if (m.type === 'aria_file') {
|
||||||
continue;
|
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 cleaned = (m.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
|
||||||
const escaped = escapeHtml(cleaned);
|
const escaped = escapeHtml(cleaned);
|
||||||
let linked = linkifyText(escaped);
|
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) => {
|
||||||
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'">`;
|
||||||
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 time = m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?';
|
const trashBtn = m.ts
|
||||||
const trashBtn = m.ts
|
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${m.ts})">🗑</button>`
|
||||||
? `<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>`;
|
||||||
const innerHtml = `${trashBtn}${linked}<div class="meta">${escapeHtml(m.meta)} — ${time}</div>`;
|
for (const b of boxes) {
|
||||||
for (const b of boxes) {
|
const el = document.createElement('div');
|
||||||
const el = document.createElement('div');
|
el.className = `chat-msg ${m.type}`;
|
||||||
el.className = `chat-msg ${m.type}`;
|
if (m.ts) el.dataset.ts = String(m.ts);
|
||||||
if (m.ts) el.dataset.ts = String(m.ts);
|
el.innerHTML = innerHtml;
|
||||||
el.innerHTML = innerHtml;
|
b.appendChild(el);
|
||||||
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;
|
for (const b of boxes) b.scrollTop = b.scrollHeight;
|
||||||
}
|
}
|
||||||
|
if (errorCount > 0) {
|
||||||
|
console.warn(`chat_history: ${errorCount} Bubble(s) konnten nicht gerendert werden`);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -701,8 +701,16 @@ function connectRVS(forcePlain) {
|
|||||||
state.rvs.lastError = err.message;
|
state.rvs.lastError = err.message;
|
||||||
broadcastState();
|
broadcastState();
|
||||||
|
|
||||||
// TLS Fallback
|
// TLS-Fallback nur bei wirklichen TLS/Handshake-Fehlern.
|
||||||
if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered) {
|
// 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;
|
fallbackTriggered = true;
|
||||||
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
|
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
|
||||||
try { ws.removeAllListeners(); ws.close(); } catch (_) {}
|
try { ws.removeAllListeners(); ws.close(); } catch (_) {}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "444:443"
|
||||||
command: caddy reverse-proxy --from ${PUBLIC_URL} --to rvs:3000
|
command: caddy reverse-proxy --from ${PUBLIC_URL} --to rvs:3000
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/caddy/data:/data # Zertifikate (PERSISTENT)
|
- ./data/caddy/data:/data # Zertifikate (PERSISTENT)
|
||||||
|
|||||||
Reference in New Issue
Block a user