diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-09 04:58:06 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-09 04:58:06 +0200 |
| commit | c9422eaf95090cde6411186c0d62b04286a8477c (patch) | |
| tree | 558378df2af6869a86215bcfe53e893b79c756bd /packages/meshbay-hub/src/meshbay_hub/api | |
| parent | 9b503785afe10849f40a26735aa08e0af24a6efc (diff) | |
| download | meshbay-c9422eaf95090cde6411186c0d62b04286a8477c.tar.gz | |
feat(hub): add web app + public group listing endpoint
webapp.py: serves / (HTML SPA) and /app.js (JS client).
app.js: login, hub API calls, node HTTP API integration,
file browser, HLS video streaming via <video> tag.
groups.py: GET /v1/groups — public group listing (no auth).
Deployed on https://meshbay.org — 200 OK. 47/47 tests.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/groups.py | 29 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/webapp.py | 74 |
2 files changed, 103 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py index 942a88e..0092fbd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py @@ -12,6 +12,35 @@ from meshbay_hub.db.models import GEKBundle, Group, GroupMember, IPLog, User router = APIRouter(prefix="/v1/groups", tags=["groups"]) +@router.get("") +async def list_public_groups( + db: AsyncSession = Depends(get_db), + limit: int = 50, + offset: int = 0, +): + """List public groups — browsable without authentication.""" + result = await db.execute( + select(Group) + .where(Group.visibility == "public", Group.status == "active") + .order_by(Group.created_at.desc()) + .limit(limit) + .offset(offset) + ) + groups = result.scalars().all() + return { + "groups": [ + { + "id": g.id, + "name": g.name, + "join_policy": g.join_policy, + "created_at": g.created_at.isoformat(), + } + for g in groups + ], + "total": len(groups), + } + + class GroupCreateRequest(BaseModel): name: str visibility: str = "private" # public|private diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py new file mode 100644 index 0000000..3d5e78b --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py @@ -0,0 +1,74 @@ +""" +Hub web application — serves the MeshBay web client at /. + +The web client (HTML/JS) is a single-page app that: + - Logs in via the hub API + - Discovers public groups + - Connects to a node URL entered by the user + - Browses the node's file index + - Downloads or streams files via the node HTTP API + +Static files are served from meshbay_hub/static/. +API routes remain at /v1/*. +""" + +from pathlib import Path + +from fastapi import APIRouter +from fastapi.responses import FileResponse, HTMLResponse + +STATIC_DIR = Path(__file__).parent.parent / "static" + +router = APIRouter(tags=["webapp"]) + + +@router.get("/app.js") +async def app_js(): + return FileResponse(STATIC_DIR / "app.js", media_type="application/javascript") + + +@router.get("/", response_class=HTMLResponse) +async def index(): + return HTMLResponse(_HTML) + + +_HTML = """\ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>MeshBay</title> + <style> + *, *::before, *::after { box-sizing: border-box; } + body { font-family: system-ui, sans-serif; margin: 0; background: #f8fafc; color: #1e293b; } + #nav { background: #0f172a; color: #e2e8f0; padding: 12px 24px; } + #nav b { color: #38bdf8; font-size: 1.2em; } + #main { max-width: 960px; margin: 32px auto; padding: 0 16px; } + h2 { color: #0f172a; margin-top: 1.5em; } + input { padding: 8px 12px; border: 1px solid #cbd5e1; border-radius: 6px; + font-size: 1em; margin: 4px; } + button { padding: 8px 16px; background: #0ea5e9; color: #fff; border: none; + border-radius: 6px; cursor: pointer; font-size: 0.9em; margin: 4px; } + button:hover { background: #0284c7; } + .card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; + padding: 16px; margin: 8px 0; cursor: pointer; } + .card:hover { border-color: #0ea5e9; } + .badge { background: #e0f2fe; color: #0284c7; padding: 2px 8px; + border-radius: 12px; font-size: 0.8em; margin-left: 8px; } + table { width: 100%; border-collapse: collapse; background: #fff; + border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; } + th { background: #f1f5f9; padding: 10px; text-align: left; } + td { padding: 10px; border-top: 1px solid #f1f5f9; } + video { border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,.15); } + a { color: #0ea5e9; text-decoration: none; } + a:hover { text-decoration: underline; } + </style> +</head> +<body> + <div id="nav"></div> + <div id="main"><p>Loading…</p></div> + <script src="/app.js"></script> +</body> +</html> +""" |