summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/ui/app.py
blob: f8978d16804a218c973da6b7c7fbc807bb1f49a1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""
MeshBay Node — local web UI (localhost:18000).

Minimal FastAPI app providing:
  GET /          → status page (HTML)
  GET /api/status → JSON status
  GET /api/files  → JSON file list from the group index
  GET /api/config → JSON config summary (no secrets)

Served only on 127.0.0.1 — not exposed to the network.
"""

import logging
from typing import TYPE_CHECKING

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

from meshbay_node import __version__

if TYPE_CHECKING:
    from meshbay_node.indexer import GroupIndex

log = logging.getLogger(__name__)


def create_ui_app(state: dict) -> FastAPI:
    """
    Create the UI FastAPI app.

    state dict is updated by the daemon and read by UI endpoints:
      state["status"]       : str  — "starting" | "running" | "error"
      state["group_id"]     : str
      state["group_name"]   : str
      state["hub_url"]      : str
      state["username"]     : str
      state["node_port"]    : int
      state["index"]        : GroupIndex | None
      state["endpoint_hint"]: str | None
    """
    app = FastAPI(
        title="MeshBay Node UI",
        version=__version__,
        docs_url=None,   # disable Swagger on local UI
        redoc_url=None,
    )

    @app.get("/api/status")
    async def api_status():
        index = state.get("index")
        return {
            "version":       __version__,
            "status":        state.get("status", "starting"),
            "hub_url":       state.get("hub_url", ""),
            "username":      state.get("username", ""),
            "group_id":      state.get("group_id", ""),
            "group_name":    state.get("group_name", ""),
            "node_port":     state.get("node_port", 0),
            "endpoint_hint": state.get("endpoint_hint"),
            "file_count":    index.count if index else 0,
            "index_version": index.version if index else 0,
        }

    @app.get("/api/files")
    async def api_files():
        index = state.get("index")
        if not index:
            return {"files": []}
        return {
            "files": [
                {
                    "id":       e.id[:16] + "…",
                    "name":     e.name,
                    "path":     e.path,
                    "size":     e.size,
                    "type":     e.type,
                    "duration": e.duration,
                }
                for e in index.entries
            ]
        }

    @app.get("/", response_class=HTMLResponse)
    async def root():
        index = state.get("index")
        status = state.get("status", "starting")
        file_count = index.count if index else 0
        status_color = {"running": "#22c55e", "error": "#ef4444"}.get(status, "#f59e0b")

        files_html = ""
        if index:
            rows = "".join(
                f"<tr><td>{e.name}</td><td>{e.type}</td>"
                f"<td>{e.size // 1024} KB</td><td>{e.path or '/'}</td></tr>"
                for e in index.entries
            )
            files_html = f"""
            <h2>Files ({file_count})</h2>
            <table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
              <thead><tr><th>Name</th><th>Type</th><th>Size</th><th>Path</th></tr></thead>
              <tbody>{rows}</tbody>
            </table>"""

        return f"""<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MeshBay Node</title>
  <style>
    body {{ font-family: monospace; max-width: 900px; margin: 40px auto; padding: 0 20px; }}
    .badge {{ display:inline-block; padding:3px 10px; border-radius:4px;
              color:#fff; background:{status_color}; font-weight:bold; }}
    table {{ font-size: 0.9em; }}
    th {{ background: #f3f4f6; }}
  </style>
  <meta http-equiv="refresh" content="10">
</head>
<body>
  <h1>🔗 MeshBay Node <span class="badge">{status}</span></h1>
  <p>
    <b>Hub:</b> {state.get("hub_url", "—")} &nbsp;|&nbsp;
    <b>User:</b> {state.get("username", "—")} &nbsp;|&nbsp;
    <b>Group:</b> {state.get("group_name") or state.get("group_id") or "—"} &nbsp;|&nbsp;
    <b>Port:</b> {state.get("node_port", "—")}
  </p>
  <p><b>Endpoint:</b> {state.get("endpoint_hint") or "unknown"}</p>
  {files_html}
  <hr>
  <small>MeshBay Node v{__version__} — <a href="/api/status">JSON status</a>
  — <a href="/api/files">JSON files</a></small>
</body>
</html>"""

    return app