summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/ui
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ui')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py95
1 files changed, 93 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py
index f8978d1..a63e28f 100644
--- a/packages/meshbay-node/src/meshbay_node/ui/app.py
+++ b/packages/meshbay-node/src/meshbay_node/ui/app.py
@@ -10,15 +10,18 @@ Minimal FastAPI app providing:
Served only on 127.0.0.1 — not exposed to the network.
"""
+import asyncio
+import json
import logging
from typing import TYPE_CHECKING
-from fastapi import FastAPI
+from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from meshbay_node import __version__
if TYPE_CHECKING:
+ from meshbay_node.chat.store import ChatStore
from meshbay_node.indexer import GroupIndex
log = logging.getLogger(__name__)
@@ -127,7 +130,95 @@ def create_ui_app(state: dict) -> FastAPI:
{files_html}
<hr>
<small>MeshBay Node v{__version__} — <a href="/api/status">JSON status</a>
- — <a href="/api/files">JSON files</a></small>
+ — <a href="/api/files">JSON files</a> — <a href="/chat">Chat</a></small>
+</body>
+</html>"""
+
+ # ── Chat endpoints ───────────────────────────────────────────────────────
+
+ _chat_subscribers: list[WebSocket] = []
+
+ @app.get("/api/chat/history")
+ async def chat_history(since: float = 0, limit: int = 100):
+ chat_store = state.get("chat_store")
+ if not chat_store:
+ return {"messages": []}
+ msgs = await chat_store.get_messages(since=since, limit=limit)
+ return {
+ "messages": [
+ {
+ "id": m.id,
+ "sender_id": m.sender_id,
+ "iteration": m.iteration,
+ "timestamp": m.timestamp,
+ "thread_id": m.thread_id,
+ }
+ for m in msgs
+ ]
+ }
+
+ @app.websocket("/ws/chat")
+ async def chat_websocket(ws: WebSocket):
+ """WebSocket for real-time chat push to the local UI."""
+ await ws.accept()
+ _chat_subscribers.append(ws)
+ try:
+ while True:
+ await ws.receive_text()
+ except WebSocketDisconnect:
+ pass
+ finally:
+ _chat_subscribers.remove(ws)
+
+ async def broadcast_chat_to_ui(msg: dict) -> None:
+ """Push a chat message to all connected UI WebSocket clients."""
+ payload = json.dumps(msg)
+ dead = []
+ for ws in _chat_subscribers:
+ try:
+ await ws.send_text(payload)
+ except Exception:
+ dead.append(ws)
+ for ws in dead:
+ _chat_subscribers.remove(ws)
+
+ app.broadcast_chat = broadcast_chat_to_ui
+
+ @app.get("/chat", response_class=HTMLResponse)
+ async def chat_page():
+ return f"""<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>MeshBay Chat</title>
+ <style>
+ body {{ font-family: monospace; max-width: 700px; margin: 40px auto; padding: 0 20px; }}
+ #messages {{ border: 1px solid #ccc; padding: 10px; height: 400px; overflow-y: auto;
+ background: #fafafa; margin-bottom: 10px; }}
+ .msg {{ margin: 4px 0; }}
+ .sender {{ font-weight: bold; color: #2563eb; }}
+ .time {{ color: #9ca3af; font-size: 0.8em; }}
+ </style>
+</head>
+<body>
+ <h1>MeshBay Chat</h1>
+ <div id="messages"></div>
+ <p><a href="/">Back to status</a></p>
+ <script>
+ const box = document.getElementById('messages');
+ const ws = new WebSocket('ws://' + location.host + '/ws/chat');
+ ws.onmessage = (e) => {{
+ const msg = JSON.parse(e.data);
+ const div = document.createElement('div');
+ div.className = 'msg';
+ const t = new Date(msg.timestamp * 1000).toLocaleTimeString();
+ div.innerHTML = '<span class="time">' + t + '</span> '
+ + '<span class="sender">' + msg.sender_id + '</span>: '
+ + '(encrypted message #' + msg.iteration + ')';
+ box.appendChild(div);
+ box.scrollTop = box.scrollHeight;
+ }};
+ </script>
</body>
</html>"""