}. 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.
diff --git a/diagnostic/index.html b/diagnostic/index.html
index 4d1705b..8b7a87e 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -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 `${match}
`;
});
const time = m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?';
- el.innerHTML = `${linked}${escapeHtml(m.meta)} β ${time}
`;
+ const trashBtn = m.ts
+ ? ``
+ : '';
+ el.innerHTML = `${trashBtn}${linked}${escapeHtml(m.meta)} β ${time}
`;
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 = ``;
}
}
- const html = `${linked}${ttsBlock}${gpsBlock}${escapeHtml(meta)} β ${new Date().toLocaleTimeString('de-DE')}
`;
+ const backupTs = options && options.backupTs;
+ const trashBtn = backupTs
+ ? ``
+ : '';
+ const html = `${trashBtn}${linked}${ttsBlock}${gpsBlock}${escapeHtml(meta)} β ${new Date().toLocaleTimeString('de-DE')}
`;
// 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;
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 8085059..afbcc51 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -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() });
diff --git a/rvs/server.js b/rvs/server.js
index 36b059e..209bbb6 100644
--- a/rvs/server.js
+++ b/rvs/server.js
@@ -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",