feat(chat): Muelltonne pro Bubble — gezielt eine Nachricht loeschen

Stefan kann jetzt einzelne Chat-Bubbles loeschen (mit Rueckfrage).
Die Bubble verschwindet aus chat_backup.jsonl (Bridge), Brain-
Conversation (rolling window + jsonl) und allen Clients (App +
Diagnostic). Genauso wichtig fuer ARIA: der gloeschte Turn ist im
naechsten Chat-Prompt nicht mehr im Window.

Pipeline:
  UI 🗑 + confirm
  → RVS delete_message_request {ts}
  → Bridge._delete_chat_message:
      - chat_backup.jsonl Zeile mit ts entfernen (atomar via tmp+rename)
      - Brain POST /conversation/delete-turn (role+content match)
      - RVS broadcast chat_message_deleted {ts}
  → App + Diagnostic entfernen Bubble lokal per ts-Match

Backend-Aenderungen:
- aria-brain/conversation.py: remove_by_match(role, content, ts_hint)
  + _rewrite_file (atomar). Match nahester Turn bei mehrfach gleichem
  content.
- aria-brain/main.py: POST /conversation/delete-turn (POST statt DELETE
  weil FastAPI keine Bodys auf DELETE erlaubt)
- bridge/aria_bridge.py: HTTP-Listener /internal/delete-chat-message
  + RVS-Handler delete_message_request. _append_chat_backup gibt jetzt
  ts zurueck, _process_core_response packt backupTs ins chat-Event.
- rvs/server.js: ALLOWED_TYPES um delete_message_request +
  chat_message_deleted erweitert.
- diagnostic/server.js: delete_chat_message-Action + chat_message_deleted
  Relay zum Browser.

Frontend-Aenderungen:
- diagnostic/index.html: 🗑 erscheint on-hover in Bubbles mit data-ts,
  confirm()-Dialog, addChat + chat_history setzen data-ts. WS-Listener
  fuer chat_message_deleted entfernt Bubble per data-ts.
- android/ChatScreen.tsx: backupTs in ChatMessage, Muelltonne-Button
  unten rechts in jeder Bubble, Alert-confirm, RVS-Listener fuer
  chat_message_deleted entfernt aus messages-State.

Live-User-Bubbles (sofort gerendert vom eigenen Send) haben noch
keinen backupTs bis der Bridge-Roundtrip durch ist — die Muelltonne
erscheint dort erst nach kurzer Verzoegerung / Reload. Folgekommit
kann das polieren wenn noetig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 16:42:20 +02:00
parent daf0d44dd7
commit 3f2499b528
7 changed files with 327 additions and 8 deletions
+62
View File
@@ -87,6 +87,11 @@ interface ChatMessage {
fires_at?: string;
condition?: string;
};
/** Backup-Timestamp aus chat_backup.jsonl auf dem Bridge — Voraussetzung
* zum Loeschen der Bubble via Muelltonne. Lokale Bubbles ohne backupTs
* sind noch nicht persistiert (kurzer Race) — Muelltonne erscheint erst
* wenn das chat_backup-Event vom Bridge zurueck kommt. */
backupTs?: number;
}
// --- Konstanten ---
@@ -415,6 +420,16 @@ const ChatScreen: React.FC = () => {
return;
}
// chat_message_deleted: Bridge hat eine Bubble aus chat_backup + Brain
// entfernt. Wir loeschen sie lokal per backupTs-Match.
if (message.type === 'chat_message_deleted') {
const ts = (message.payload || {}).ts;
if (typeof ts !== 'number') return;
console.log(`[Chat] chat_message_deleted ts=${ts}`);
setMessages(prev => prev.filter(m => m.backupTs !== ts));
return;
}
// chat_history_response: kompletter Server-Stand. App ersetzt ihre
// persistierte Chat-History damit. Lokal-only Bubbles (laufende
// Voice-Aufnahmen ohne STT-Result, Skill-Created-Events ohne
@@ -440,6 +455,7 @@ const ChatScreen: React.FC = () => {
text: m.text || '',
timestamp: m.ts || Date.now(),
attachments: attachments.length ? attachments : undefined,
backupTs: typeof m.ts === 'number' ? m.ts : undefined,
};
});
const maxTs = incoming.reduce((mx: number, m: any) => Math.max(mx, m.ts || 0), 0);
@@ -654,6 +670,7 @@ const ChatScreen: React.FC = () => {
timestamp: ts,
attachments: message.payload.attachments as Attachment[] | undefined,
messageId: (message.payload.messageId as string) || undefined,
backupTs: (message.payload.backupTs as number) || undefined,
};
return capMessages([...prev, ariaMsg]);
});
@@ -1386,11 +1403,41 @@ const ChatScreen: React.FC = () => {
<Text style={styles.playButtonText}>{'\uD83D\uDD0A'}</Text>
</TouchableOpacity>
)}
{item.backupTs ? (
<TouchableOpacity
style={styles.bubbleTrash}
hitSlop={{top:6,bottom:6,left:6,right:6}}
onPress={() => confirmDeleteBubble(item)}
>
<Text style={styles.bubbleTrashIcon}>{'🗑'}</Text>
</TouchableOpacity>
) : null}
<Text style={styles.timestamp}>{time}</Text>
</View>
);
};
const confirmDeleteBubble = (item: ChatMessage) => {
const ts = item.backupTs;
if (!ts) return;
const preview = (item.text || '').slice(0, 80) || '(leere Bubble)';
Alert.alert(
'Bubble loeschen?',
`"${preview}${item.text && item.text.length > 80 ? '…' : ''}"\n\nWird aus chat_backup, Brain-Konversation und allen Clients entfernt.`,
[
{ text: 'Abbrechen', style: 'cancel' },
{
text: 'Loeschen',
style: 'destructive',
onPress: () => {
console.log(`[Chat] delete_message_request ts=${ts}`);
rvs.send('delete_message_request' as any, { ts });
},
},
],
);
};
const connectionDotColor =
connectionState === 'connected' ? '#34C759' :
connectionState === 'connecting' ? '#FFD60A' : '#FF3B30';
@@ -1967,6 +2014,21 @@ const styles = StyleSheet.create({
playButtonText: {
fontSize: 16,
},
bubbleTrash: {
position: 'absolute',
top: 4,
right: 6,
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: 'rgba(255,59,48,0.18)',
alignItems: 'center',
justifyContent: 'center',
},
bubbleTrashIcon: {
fontSize: 12,
color: '#FF6B6B',
},
fullscreenOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.95)',
+49
View File
@@ -121,6 +121,55 @@ class Conversation:
self.turns = []
logger.warning("Konversation komplett zurueckgesetzt")
def _rewrite_file(self) -> None:
"""Datei komplett aus In-Memory-State neu schreiben.
Wird nach Mutationen (Loeschen) genutzt. Alte distill-Marker
gehen dabei verloren — das ist OK weil der In-Memory-State
bereits post-distill ist."""
try:
CONVERSATION_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp = CONVERSATION_FILE.with_suffix(".jsonl.tmp")
with tmp.open("w", encoding="utf-8") as f:
for t in self.turns:
f.write(json.dumps({
"ts": t.ts, "role": t.role,
"content": t.content, "source": t.source,
}, ensure_ascii=False) + "\n")
tmp.replace(CONVERSATION_FILE)
except Exception as exc:
logger.warning("Konversation rewrite fehlgeschlagen: %s", exc)
def remove_by_match(self, role: str, content: str,
ts_iso_hint: Optional[str] = None) -> bool:
"""Entfernt EINEN Turn mit passendem role + content.
Bei Mehrfach-Match (z.B. zwei identische 'ja'-Turns) waehlt
den naehesten zum ts_iso_hint, sonst den juengsten.
Returns True wenn was entfernt wurde.
"""
candidates = [(i, t) for i, t in enumerate(self.turns)
if t.role == role and t.content == content]
if not candidates:
logger.info("[conv] remove_by_match: kein Match fuer role=%s content[:40]=%r",
role, content[:40])
return False
if len(candidates) > 1 and ts_iso_hint:
def _diff(item):
_, turn = item
try:
return abs((datetime.fromisoformat(turn.ts.replace("Z", "+00:00"))
- datetime.fromisoformat(ts_iso_hint.replace("Z", "+00:00"))).total_seconds())
except Exception:
return 1e9
candidates.sort(key=_diff)
idx, turn = candidates[0] if not ts_iso_hint else candidates[0]
self.turns.pop(idx)
self._rewrite_file()
logger.info("[conv] Turn entfernt: role=%s ts=%s content[:40]=%r",
turn.role, turn.ts, turn.content[:40])
return True
def stats(self) -> dict:
return {
"turns": len(self.turns),
+22
View File
@@ -432,6 +432,28 @@ def conversation_reset():
return {"ok": True, "turns": 0}
class ConvDeleteBody(BaseModel):
role: str
content: str
ts_iso_hint: Optional[str] = None
@app.post("/conversation/delete-turn")
def conversation_delete_turn(body: ConvDeleteBody):
"""Entfernt einen einzelnen Turn aus dem Rolling-Window + jsonl.
Match per role + content (erstes Vorkommen wenn ts_iso_hint None,
sonst nahester zur Zeit). 404 wenn kein Match.
POST statt DELETE weil FastAPI 0.115 keine Bodys auf DELETE
erlaubt — semantisch trotzdem eine Loeschung."""
ok = conversation().remove_by_match(
role=body.role, content=body.content, ts_iso_hint=body.ts_iso_hint,
)
if not ok:
raise HTTPException(404, "Turn mit diesem role+content nicht gefunden")
return {"ok": True, "turns": len(conversation().turns)}
@app.post("/conversation/distill")
def conversation_distill_now():
"""Manueller Trigger fuer Destillat — fuer Tests oder vor einem
+125 -4
View File
@@ -958,18 +958,21 @@ class ARIABridge:
Watcher: last_user_message_ago_sec basiert darauf."""
self._persist_state("activity", {"last_user_ts": int(time.time())})
def _append_chat_backup(self, entry: dict) -> None:
def _append_chat_backup(self, entry: dict) -> int:
"""Schreibt eine Zeile in /shared/config/chat_backup.jsonl.
Wird von Diagnostic + App als History-Quelle gelesen.
entry braucht mindestens {role, text}; ts wird ergaenzt."""
entry braucht mindestens {role, text}; ts wird ergaenzt.
Returns den ts (auch fuer Bubble-Loeschen-Tracking)."""
ts = int(asyncio.get_event_loop().time() * 1000)
try:
line = {"ts": int(asyncio.get_event_loop().time() * 1000)}
line = {"ts": ts}
line.update(entry)
Path("/shared/config").mkdir(parents=True, exist_ok=True)
with open("/shared/config/chat_backup.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(line, ensure_ascii=False) + "\n")
except Exception as e:
logger.warning("[backup] chat_backup-Write fehlgeschlagen: %s", e)
return ts
def _read_chat_backup_since(self, since_ms: int, limit: int = 100) -> list[dict]:
"""Liest chat_backup.jsonl, gibt Eintraege > since_ms zurueck, max limit neueste.
@@ -1043,7 +1046,7 @@ class ARIABridge:
# Antwort in chat_backup.jsonl loggen (gecleanter Text, ohne File-Marker)
# File-Marker werden separat als file_from_aria-Events ausgeliefert.
self._append_chat_backup({
assistant_backup_ts = self._append_chat_backup({
"role": "assistant",
"text": text,
"files": [{"serverPath": f["serverPath"], "name": f["name"],
@@ -1079,6 +1082,9 @@ class ARIABridge:
"text": text,
"sender": "aria",
"messageId": message_id,
# backupTs = der ts in chat_backup.jsonl. Wird von Clients als
# Bubble-ID fuer das Mülltonne-Loeschen verwendet (delete_message_request).
"backupTs": assistant_backup_ts,
# Debug: aufbereiteter Text fuer TTS (App ignoriert, Diagnostic zeigt optional)
"ttsText": tts_text_preview if tts_text_preview != text else "",
},
@@ -1792,6 +1798,21 @@ class ARIABridge:
})
return
elif msg_type == "delete_message_request":
# App oder Diagnostic loescht eine einzelne Bubble.
# payload: {ts: <chat_backup-ts>}. Bridge entfernt aus
# chat_backup.jsonl + Brain conversation.jsonl, broadcastet
# danach chat_message_deleted an alle Clients.
ts = payload.get("ts")
if not isinstance(ts, (int, float)):
logger.warning("[rvs] delete_message_request ohne valide ts: %r", payload)
return
logger.info("[rvs] delete_message_request ts=%s", ts)
result = await self._delete_chat_message(int(ts))
if not result.get("ok"):
logger.warning("[rvs] delete_message fehlgeschlagen: %s", result.get("error"))
return
elif msg_type == "file_list_request":
# App fragt die Liste aller /shared/uploads/-Dateien an.
logger.info("[rvs] file_list_request von App")
@@ -2455,6 +2476,21 @@ class ARIABridge:
self._handle_trigger_fired(reply, trigger_name, ttype, events)
)
await _send_response(writer, 200, {"ok": True})
elif method == "POST" and path == "/internal/delete-chat-message":
try:
data = json.loads(body.decode("utf-8", "ignore"))
except Exception as exc:
await _send_response(writer, 400, {"error": f"bad json: {exc}"})
return
ts = data.get("ts")
if not isinstance(ts, (int, float)):
await _send_response(writer, 400, {"error": "ts (number) erforderlich"})
return
result = await self._delete_chat_message(int(ts))
if result.get("ok"):
await _send_response(writer, 200, result)
else:
await _send_response(writer, 404, result)
elif method == "GET" and path == "/health":
await _send_response(writer, 200, {"ok": True, "service": "bridge-internal"})
else:
@@ -2482,6 +2518,91 @@ class ARIABridge:
except Exception:
logger.exception("[bridge] Internal HTTP-Listener konnte nicht starten")
async def _delete_chat_message(self, ts: int) -> dict:
"""Entfernt eine Bubble: aus chat_backup.jsonl + Brain conversation,
broadcastet chat_message_deleted via RVS.
Returns {ok, role, content_preview} oder {ok:False, error}.
"""
path = Path("/shared/config/chat_backup.jsonl")
if not path.exists():
return {"ok": False, "error": "chat_backup.jsonl existiert nicht"}
try:
lines = path.read_text(encoding="utf-8").splitlines()
except Exception as exc:
return {"ok": False, "error": f"Lesen fehlgeschlagen: {exc}"}
kept: list[str] = []
removed_entry: Optional[dict] = None
for raw in lines:
raw = raw.strip()
if not raw:
continue
try:
obj = json.loads(raw)
except Exception:
kept.append(raw)
continue
if obj.get("ts") == ts and removed_entry is None:
removed_entry = obj
continue
kept.append(raw)
if removed_entry is None:
return {"ok": False, "error": f"Kein Eintrag mit ts={ts} gefunden"}
# chat_backup.jsonl neu schreiben (atomar via tmp)
try:
tmp = path.with_suffix(".jsonl.tmp")
tmp.write_text("\n".join(kept) + ("\n" if kept else ""), encoding="utf-8")
tmp.replace(path)
except Exception as exc:
return {"ok": False, "error": f"Schreiben fehlgeschlagen: {exc}"}
role = removed_entry.get("role", "")
content = removed_entry.get("text", "")
logger.info("[chat-del] chat_backup ts=%s role=%s content[:40]=%r entfernt",
ts, role, content[:40])
# Brain conversation.jsonl auch entrümpeln (best-effort).
# ts in chat_backup ist asyncio-loop-time-ms, im Brain ist's eine ISO-UTC-Time.
# Die kann man nicht direkt mappen — wir uebergeben nur role+content
# und hoffen dass das eindeutig matched. Bei mehrfach gleichem content
# entfernt remove_by_match den juengsten passenden Turn.
if role in ("user", "assistant") and content:
try:
brain_url = os.environ.get("BRAIN_URL", "http://aria-brain:8080")
payload = json.dumps({"role": role, "content": content}).encode("utf-8")
def _post():
req = urllib.request.Request(
f"{brain_url}/conversation/delete-turn",
data=payload, method="POST",
headers={"Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=10) as r:
return r.status
except urllib.error.HTTPError as e:
return e.code
except Exception:
return None
status = await asyncio.get_event_loop().run_in_executor(None, _post)
logger.info("[chat-del] Brain conversation/delete-turn → %s", status)
except Exception as exc:
logger.warning("[chat-del] Brain-Call fehlgeschlagen: %s", exc)
# RVS-Broadcast damit alle Clients die Bubble entfernen
try:
await self._send_to_rvs({
"type": "chat_message_deleted",
"payload": {"ts": ts, "role": role},
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
except Exception as exc:
logger.warning("[chat-del] RVS-Broadcast fehlgeschlagen: %s", exc)
return {"ok": True, "role": role, "content_preview": content[:80]}
async def _handle_trigger_fired(self, reply: str, trigger_name: str,
ttype: str, events: list) -> None:
"""Spiegelt eine Brain-Trigger-Antwort wie eine normale ARIA-Antwort.
+51 -4
View File
@@ -67,7 +67,13 @@
padding: 12px; margin-bottom: 8px; display: flex; flex-direction: column; gap: 8px; }
.chat-msg { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.5;
word-wrap: break-word; max-width: 80%; white-space: pre-wrap;
box-shadow: 0 1px 2px rgba(0,0,0,0.4); }
box-shadow: 0 1px 2px rgba(0,0,0,0.4); position: relative; }
.chat-msg .bubble-trash { position:absolute; top:4px; right:6px; background:rgba(255,59,48,0.15);
color:#FF6B6B; border:none; border-radius:50%; width:22px; height:22px;
font-size:12px; line-height:18px; padding:0; cursor:pointer; opacity:0;
transition:opacity 0.15s; }
.chat-msg:hover .bubble-trash { opacity: 1; }
.chat-msg .bubble-trash:hover { background:#FF3B30; color:#fff; }
.chat-msg.sent { background: #0096FF; color: #fff; align-self: flex-end;
border-bottom-right-radius: 4px; }
.chat-msg.received { background: #1E1E2E; color: #E8E8F0; align-self: flex-start;
@@ -1378,7 +1384,23 @@
chatType = 'sent';
label = `via RVS (${sender})`;
}
addChat(chatType, p.text || '?', label, { location: p.location });
addChat(chatType, p.text || '?', label, {
location: p.location,
ttsText: p.ttsText,
backupTs: p.backupTs,
});
return;
}
if (msg.type === 'chat_message_deleted') {
// Bridge meldet: Bubble wurde aus chat_backup + Brain entfernt.
// Bubble lokal entfernen (data-ts-Match in beiden Chat-Boxen).
const ts = msg.payload?.ts;
if (!ts) return;
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const el = box.querySelector(`.chat-msg[data-ts="${ts}"]`);
if (el) el.remove();
}
return;
}
if (msg.type === 'proxy_result') {
@@ -1453,6 +1475,7 @@
}
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);
@@ -1463,7 +1486,10 @@
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') : '?';
el.innerHTML = `${linked}<div class="meta">${escapeHtml(m.meta)}${time}</div>`;
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);
}
chatBox.scrollTop = chatBox.scrollHeight;
@@ -1492,6 +1518,22 @@
}
}
/** Loescht eine einzelne Chat-Bubble (mit Rueckfrage).
* Backend (Bridge) raeumt chat_backup.jsonl + Brain-Conversation
* und broadcastet danach chat_message_deleted — wir entfernen die
* Bubble lokal erst dann, nicht optimistisch. */
function deleteDiagBubble(ts) {
if (!ts) return;
let preview = '';
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const el = box.querySelector(`.chat-msg[data-ts="${ts}"]`);
if (el) { preview = (el.textContent || '').slice(0, 80); break; }
}
if (!confirm(`Diese Bubble wirklich loeschen?\n\n"${preview}…"\n\nWird aus chat_backup, Brain-Konversation und allen Clients entfernt.`)) return;
send({ action: 'delete_chat_message', ts });
}
function sendDiagAttachments() {
// Alle pending Dateien an RVS senden
for (const f of diagPendingFiles) {
@@ -1781,7 +1823,11 @@
gpsBlock = `<div style="margin-top:6px;padding:4px 8px;background:rgba(52,199,89,0.08);border-left:2px solid #34C759;font-size:11px;color:#88BB99;"><span style="color:#34C759;font-weight:bold;">📍 GPS:</span> <a href="${mapLink}" target="_blank" rel="noopener" style="color:#88BB99;text-decoration:underline;">${lat}, ${lon}</a></div>`;
}
}
const html = `${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`;
const backupTs = options && options.backupTs;
const trashBtn = backupTs
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${backupTs})">🗑</button>`
: '';
const html = `${trashBtn}${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`;
// Thinking-Indikator ausblenden bei neuer Nachricht
updateThinkingIndicator({ activity: 'idle' });
@@ -1791,6 +1837,7 @@
if (!box) continue;
const el = document.createElement('div');
el.className = `chat-msg ${type}`;
if (backupTs) el.dataset.ts = String(backupTs);
el.innerHTML = html;
box.appendChild(el);
box.scrollTop = box.scrollHeight;
+17
View File
@@ -617,6 +617,12 @@ function connectRVS(forcePlain) {
// Mode-Broadcast von der Bridge → an Browser-Clients weiterreichen
log("info", "rvs", `Mode-Broadcast: ${msg.payload?.mode} (${msg.payload?.name})`);
broadcast({ type: "mode", payload: msg.payload });
} else if (msg.type === "chat_message_deleted") {
// Bridge meldet: Bubble wurde aus chat_backup + Brain entfernt.
// An Browser-Clients weiterreichen damit sie die Bubble lokal entfernen.
const ts = msg.payload?.ts;
log("info", "rvs", `chat_message_deleted ts=${ts}`);
broadcast({ type: "chat_message_deleted", payload: msg.payload });
} else if (msg.type === "voice_ready") {
// XTTS-Bridge meldet Stimme fertig geladen → an Browser durchreichen
const v = msg.payload?.voice || "";
@@ -1835,6 +1841,17 @@ wss.on("connection", (ws) => {
// Weiterleiten an XTTS-Bridge, die antwortet mit neuer Liste
sendToRVS_raw({ type: "xtts_delete_voice", payload: { name: msg.name }, timestamp: Date.now() });
log("info", "server", `Voice-Delete '${msg.name}' an XTTS-Bridge gesendet`);
} else if (msg.action === "delete_chat_message") {
// Bubble loeschen — Bridge raeumt chat_backup.jsonl + Brain-conversation
// + broadcastet chat_message_deleted via RVS.
const ts = Number(msg.ts);
if (!Number.isFinite(ts)) {
ws.send(JSON.stringify({ type: "log", level: "error", source: "server",
message: `delete_chat_message: ungueltiges ts=${msg.ts}` }));
return;
}
sendToRVS_raw({ type: "delete_message_request", payload: { ts }, timestamp: Date.now() });
log("info", "server", `delete_message_request ts=${ts} an Bridge gesendet`);
} else if (msg.action === "set_mode") {
// Mode-Wechsel → Bridge bearbeitet und broadcastet an alle Clients
sendToRVS_raw({ type: "mode", payload: { mode: msg.mode }, timestamp: Date.now() });
+1
View File
@@ -28,6 +28,7 @@ const ALLOWED_TYPES = new Set([
"trigger_created",
"location_update", "location_tracking",
"chat_history_request", "chat_history_response", "chat_cleared",
"delete_message_request", "chat_message_deleted",
"file_delete_batch_request", "file_delete_batch_response",
"file_zip_request", "file_zip_response",
"xtts_delete_voice",