diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ui/app.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ui/app.py | 95 |
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>""" |