Rich management GUI: profile detail, address+QR, chat, edit, groups

Supervisor gains normalized helpers (get_profile/address, update_profile,
contacts, groups, history, send, create/leave/delete/join group, clear chat,
delete contact) exposed as REST. Front end rebuilt to mirror simplex-manager:
- sidebar layout; profiles list + per-profile detail page
- Profile card + Edit dialog (display name / full name / bio)
- Address card: SMP link + copy + QR (qrcode) + scan caption
- Contacts (Chat / Clear / Delete), Groups (Create, Chat / Link / Leave / Delete,
  Join when invited), Create Channel (observer link)
- in-GUI chat view: history + composer with live polling
- live Event Log per profile over the /events WebSocket

Validated via running server: address shown, profile edit persists, group create
returns link. (json/JSONResponse imports fixed.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-06-04 13:07:48 +01:00
parent d6041c1048
commit d098b1d6ce
3 changed files with 512 additions and 259 deletions

View File

@@ -13,7 +13,7 @@ import contextlib
from pathlib import Path
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, JSONResponse
from .supervisor import Supervisor
@@ -78,6 +78,100 @@ async def cmd(name: str, body: dict) -> dict:
return {"resp": await sup.send(name, body["cmd"])}
# ── High-level profile operations (normalized for the GUI) ───────────────────────
async def _json(coro):
"""Await a helper coroutine; return its result as JSON, or {error} on failure."""
try:
return JSONResponse(await coro)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=400)
@app.get("/profiles/{name}/profile")
async def get_profile(name: str):
return await _json(sup.get_profile(name))
@app.post("/profiles/{name}/profile")
async def set_profile(name: str, body: dict):
async def do():
await sup.update_profile(name, body.get("display_name", name),
body.get("full_name", ""), body.get("bio", ""))
return {"ok": True}
return await _json(do())
@app.get("/profiles/{name}/contacts")
async def contacts(name: str):
return await _json(sup.get_contacts(name))
@app.get("/profiles/{name}/groups")
async def groups(name: str):
return await _json(sup.get_groups(name))
@app.post("/profiles/{name}/groups")
async def create_group(name: str, body: dict):
return await _json(sup.create_group(name, body["name"], bool(body.get("observer"))))
@app.get("/profiles/{name}/groups/{gid}/link")
async def group_link(name: str, gid: int):
async def do():
return {"link": await sup.group_link(name, gid)}
return await _json(do())
@app.post("/profiles/{name}/groups/{gid}/leave")
async def leave_group(name: str, gid: int):
async def do():
await sup.leave_group(name, gid); return {"ok": True}
return await _json(do())
@app.post("/profiles/{name}/groups/{gid}/join")
async def join_group(name: str, gid: int):
async def do():
await sup.join_group(name, gid); return {"ok": True}
return await _json(do())
@app.delete("/profiles/{name}/groups/{gid}")
async def delete_group(name: str, gid: int):
async def do():
await sup.delete_group(name, gid); return {"ok": True}
return await _json(do())
@app.get("/profiles/{name}/chat/{chat_type}/{chat_id}/history")
async def history(name: str, chat_type: str, chat_id: int, count: int = 50):
async def do():
return {"messages": await sup.get_history(name, chat_type, chat_id, count)}
return await _json(do())
@app.post("/profiles/{name}/chat/{chat_type}/{chat_id}/send")
async def chat_send(name: str, chat_type: str, chat_id: int, body: dict):
async def do():
await sup.send_message(name, chat_type, chat_id, body["text"]); return {"ok": True}
return await _json(do())
@app.post("/profiles/{name}/chat/{chat_type}/{chat_id}/clear")
async def chat_clear(name: str, chat_type: str, chat_id: int):
async def do():
await sup.clear_chat(name, chat_type, chat_id); return {"ok": True}
return await _json(do())
@app.delete("/profiles/{name}/contacts/{contact_id}")
async def contact_delete(name: str, contact_id: int):
async def do():
await sup.delete_contact(name, contact_id); return {"ok": True}
return await _json(do())
@app.post("/profiles/{name}/stop")
async def stop(name: str) -> dict:
await sup.stop(name)