diff --git a/README.md b/README.md index 131ceeae..3d40ca54 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,11 @@ If extending, have your LLM read the three `AGENTS.md` files: `./AGENTS.md`, `./ ## Requirements - Python 3.10+ -- Node.js LTS or current (20, 22, 24, 25) - [UV](https://astral.sh/uv) package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh` - MeshCore radio connected via USB serial, TCP, or BLE +Node.js is not required to run the app when using the bundled prebuilt frontend. You only need Node>=20 + recent NPM if you want to develop the frontend or rebuild the web UI from source. +
Finding your serial port @@ -76,9 +77,6 @@ cd Remote-Terminal-for-MeshCore # Install backend dependencies uv sync -# Build frontend -cd frontend && npm install && npm run build && cd .. - # Run server uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` @@ -101,16 +99,16 @@ $env:MESHCORE_SERIAL_PORT="COM8" # or your COM port uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` -Access at http://localhost:8000 +Access at http://localhost:8000. > **Note:** WebGPU cracking requires HTTPS when not on localhost. See the HTTPS section under Additional Setup. +> +> If you're running from a source checkout and want to rebuild the UI yourself, install Node.js and run `cd frontend && npm install && npm run build`. The backend prefers `frontend/dist` when present and otherwise falls back to the bundled prebuilt frontend. ## Docker Compose > **Warning:** Docker has intermittent issues with serial event subscriptions. The native method above is more reliable. -> **Note:** BLE-in-docker is outside the scope of this README, but the env vars should all still work. - Edit `docker-compose.yaml` to set a serial device for passthrough, or uncomment your transport (serial or TCP). Then: ```bash @@ -177,7 +175,7 @@ uv run uvicorn app.main:app --reload cd frontend npm install npm run dev # Dev server at http://localhost:5173 (proxies API to :8000) -npm run build # Production build to dist/ +npm run build # Production build to dist/ (used preferentially) and prebuilt/ (bundled to save low-memory friends from needing to build) ``` Run both the backend and `npm run dev` for hot-reloading frontend development. @@ -207,7 +205,7 @@ cd frontend npm run lint:fix # esLint + auto-fix npm run test:run # run tests npm run format # prettier (always writes) -npm run build # build the frontend +npm run build # build frontend/dist and frontend/prebuilt ```
@@ -298,11 +296,6 @@ cd /opt/remoteterm sudo -u remoteterm uv venv sudo -u remoteterm uv sync -# Build frontend (required for the backend to serve the web UI) -cd /opt/remoteterm/frontend -sudo -u remoteterm npm install -sudo -u remoteterm npm run build - # Install and start service sudo cp /opt/remoteterm/remoteterm.service /etc/systemd/system/ sudo systemctl daemon-reload @@ -314,6 +307,8 @@ sudo journalctl -u remoteterm -f ``` Edit `/etc/systemd/system/remoteterm.service` to set `MESHCORE_SERIAL_PORT` if needed. + +If you are deploying from a source checkout that does not include the bundled prebuilt frontend, install Node.js and run `cd /opt/remoteterm/frontend && sudo -u remoteterm npm install && sudo -u remoteterm npm run build` before starting the service.
diff --git a/app/frontend_static.py b/app/frontend_static.py index ddb0109c..45bb6777 100644 --- a/app/frontend_static.py +++ b/app/frontend_static.py @@ -10,6 +10,7 @@ logger = logging.getLogger(__name__) INDEX_CACHE_CONTROL = "no-store" ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable" STATIC_FILE_CACHE_CONTROL = "public, max-age=3600" +FRONTEND_BUILD_INSTRUCTIONS = "Run 'cd frontend && npm install && npm run build'." class CacheControlStaticFiles(StaticFiles): @@ -48,40 +49,38 @@ def _resolve_request_origin(request: Request) -> str: return str(request.base_url).rstrip("/") -def register_frontend_static_routes(app: FastAPI, frontend_dir: Path) -> bool: - """Register frontend static file routes if a built frontend is available. - - Returns True when routes are registered, False when frontend files are - missing/incomplete. Missing frontend files are logged but are not fatal. - """ +def _validate_frontend_dir(frontend_dir: Path, *, log_failures: bool = True) -> tuple[bool, Path]: + """Resolve and validate a built frontend directory.""" frontend_dir = frontend_dir.resolve() index_file = frontend_dir / "index.html" - assets_dir = frontend_dir / "assets" if not frontend_dir.exists(): - logger.error( - "Frontend build directory not found at %s. " - "Run 'cd frontend && npm run build'. API will continue without frontend routes.", - frontend_dir, - ) - return False + if log_failures: + logger.error("Frontend build directory not found at %s.", frontend_dir) + return False, frontend_dir if not frontend_dir.is_dir(): - logger.error( - "Frontend build path is not a directory: %s. " - "API will continue without frontend routes.", - frontend_dir, - ) - return False + if log_failures: + logger.error("Frontend build path is not a directory: %s.", frontend_dir) + return False, frontend_dir if not index_file.exists(): - logger.error( - "Frontend index file not found at %s. " - "Run 'cd frontend && npm run build'. API will continue without frontend routes.", - index_file, - ) + if log_failures: + logger.error("Frontend index file not found at %s.", index_file) + return False, frontend_dir + + return True, frontend_dir + + +def register_frontend_static_routes(app: FastAPI, frontend_dir: Path) -> bool: + """Register frontend static file routes if a built frontend is available.""" + valid, frontend_dir = _validate_frontend_dir(frontend_dir) + if not valid: return False + index_file = frontend_dir / "index.html" + assets_dir = frontend_dir / "assets" + if assets_dir.exists() and assets_dir.is_dir(): app.mount( "/assets", @@ -157,6 +156,30 @@ def register_frontend_static_routes(app: FastAPI, frontend_dir: Path) -> bool: return True +def register_first_available_frontend_static_routes( + app: FastAPI, frontend_dirs: list[Path] +) -> Path | None: + """Register frontend routes from the first valid build directory.""" + for i, candidate in enumerate(frontend_dirs): + valid, resolved_candidate = _validate_frontend_dir(candidate, log_failures=False) + if not valid: + continue + + if register_frontend_static_routes(app, resolved_candidate): + logger.info("Selected frontend build directory %s", resolved_candidate) + return resolved_candidate + + if i < len(frontend_dirs) - 1: + logger.warning("Frontend build at %s was unusable; trying fallback", resolved_candidate) + + logger.error( + "No usable frontend build found. Searched: %s. %s API will continue without frontend routes.", + ", ".join(str(path.resolve()) for path in frontend_dirs), + FRONTEND_BUILD_INSTRUCTIONS, + ) + return None + + def register_frontend_missing_fallback(app: FastAPI) -> None: """Register a fallback route that tells the user to build the frontend.""" @@ -164,7 +187,5 @@ def register_frontend_missing_fallback(app: FastAPI) -> None: async def frontend_not_built(): return JSONResponse( status_code=404, - content={ - "detail": "Frontend not built. Run: cd frontend && npm install && npm run build" - }, + content={"detail": f"Frontend not built. {FRONTEND_BUILD_INSTRUCTIONS}"}, ) diff --git a/app/main.py b/app/main.py index 35d499cc..2a03f5d7 100644 --- a/app/main.py +++ b/app/main.py @@ -11,7 +11,10 @@ from fastapi.responses import JSONResponse from app.config import settings as server_settings from app.config import setup_logging from app.database import db -from app.frontend_static import register_frontend_missing_fallback, register_frontend_static_routes +from app.frontend_static import ( + register_first_available_frontend_static_routes, + register_frontend_missing_fallback, +) from app.radio import RadioDisconnectedError from app.radio_sync import ( stop_message_polling, @@ -151,6 +154,9 @@ app.include_router(statistics.router, prefix="/api") app.include_router(ws.router, prefix="/api") # Serve frontend static files in production -FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist" -if not register_frontend_static_routes(app, FRONTEND_DIR): +FRONTEND_DIST_DIR = Path(__file__).parent.parent / "frontend" / "dist" +FRONTEND_PREBUILT_DIR = Path(__file__).parent.parent / "frontend" / "prebuilt" +if not register_first_available_frontend_static_routes( + app, [FRONTEND_DIST_DIR, FRONTEND_PREBUILT_DIR] +): register_frontend_missing_fallback(app) diff --git a/frontend/package.json b/frontend/package.json index 7794dad1..b7962e33 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc && vite build && vite build --outDir prebuilt", "preview": "vite preview", "test": "vitest", "test:run": "vitest run", diff --git a/frontend/prebuilt/apple-touch-icon.png b/frontend/prebuilt/apple-touch-icon.png new file mode 100644 index 00000000..81c36f5a Binary files /dev/null and b/frontend/prebuilt/apple-touch-icon.png differ diff --git a/frontend/prebuilt/assets/BotCodeEditor-CBFX17Kn.js b/frontend/prebuilt/assets/BotCodeEditor-CBFX17Kn.js new file mode 100644 index 00000000..344d57af Binary files /dev/null and b/frontend/prebuilt/assets/BotCodeEditor-CBFX17Kn.js differ diff --git a/frontend/prebuilt/assets/BotCodeEditor-CBFX17Kn.js.map b/frontend/prebuilt/assets/BotCodeEditor-CBFX17Kn.js.map new file mode 100644 index 00000000..4810b403 Binary files /dev/null and b/frontend/prebuilt/assets/BotCodeEditor-CBFX17Kn.js.map differ diff --git a/frontend/prebuilt/assets/CrackerPanel-RTfofPO9.js b/frontend/prebuilt/assets/CrackerPanel-RTfofPO9.js new file mode 100644 index 00000000..24374ab6 Binary files /dev/null and b/frontend/prebuilt/assets/CrackerPanel-RTfofPO9.js differ diff --git a/frontend/prebuilt/assets/CrackerPanel-RTfofPO9.js.map b/frontend/prebuilt/assets/CrackerPanel-RTfofPO9.js.map new file mode 100644 index 00000000..5021e95c Binary files /dev/null and b/frontend/prebuilt/assets/CrackerPanel-RTfofPO9.js.map differ diff --git a/frontend/prebuilt/assets/MapView-DG71Do9A.js b/frontend/prebuilt/assets/MapView-DG71Do9A.js new file mode 100644 index 00000000..7050c294 Binary files /dev/null and b/frontend/prebuilt/assets/MapView-DG71Do9A.js differ diff --git a/frontend/prebuilt/assets/MapView-DG71Do9A.js.map b/frontend/prebuilt/assets/MapView-DG71Do9A.js.map new file mode 100644 index 00000000..36364688 Binary files /dev/null and b/frontend/prebuilt/assets/MapView-DG71Do9A.js.map differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-D6bvxuvI.js b/frontend/prebuilt/assets/NeighborsMiniMap-D6bvxuvI.js new file mode 100644 index 00000000..65aca506 Binary files /dev/null and b/frontend/prebuilt/assets/NeighborsMiniMap-D6bvxuvI.js differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-D6bvxuvI.js.map b/frontend/prebuilt/assets/NeighborsMiniMap-D6bvxuvI.js.map new file mode 100644 index 00000000..8b1d046e Binary files /dev/null and b/frontend/prebuilt/assets/NeighborsMiniMap-D6bvxuvI.js.map differ diff --git a/frontend/prebuilt/assets/PathRouteMap-DiGw6Qka.js b/frontend/prebuilt/assets/PathRouteMap-DiGw6Qka.js new file mode 100644 index 00000000..cb9632d8 Binary files /dev/null and b/frontend/prebuilt/assets/PathRouteMap-DiGw6Qka.js differ diff --git a/frontend/prebuilt/assets/PathRouteMap-DiGw6Qka.js.map b/frontend/prebuilt/assets/PathRouteMap-DiGw6Qka.js.map new file mode 100644 index 00000000..1a0c65e0 Binary files /dev/null and b/frontend/prebuilt/assets/PathRouteMap-DiGw6Qka.js.map differ diff --git a/frontend/prebuilt/assets/Popup-JtfOLvZE.js b/frontend/prebuilt/assets/Popup-JtfOLvZE.js new file mode 100644 index 00000000..460ebc75 Binary files /dev/null and b/frontend/prebuilt/assets/Popup-JtfOLvZE.js differ diff --git a/frontend/prebuilt/assets/Popup-JtfOLvZE.js.map b/frontend/prebuilt/assets/Popup-JtfOLvZE.js.map new file mode 100644 index 00000000..10a1dd64 Binary files /dev/null and b/frontend/prebuilt/assets/Popup-JtfOLvZE.js.map differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-bua8zxo_.js b/frontend/prebuilt/assets/RepeaterDashboard-bua8zxo_.js new file mode 100644 index 00000000..ff80783b Binary files /dev/null and b/frontend/prebuilt/assets/RepeaterDashboard-bua8zxo_.js differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-bua8zxo_.js.map b/frontend/prebuilt/assets/RepeaterDashboard-bua8zxo_.js.map new file mode 100644 index 00000000..3a16d650 Binary files /dev/null and b/frontend/prebuilt/assets/RepeaterDashboard-bua8zxo_.js.map differ diff --git a/frontend/prebuilt/assets/SearchView-Cs7v3m6K.js b/frontend/prebuilt/assets/SearchView-Cs7v3m6K.js new file mode 100644 index 00000000..03fb6788 Binary files /dev/null and b/frontend/prebuilt/assets/SearchView-Cs7v3m6K.js differ diff --git a/frontend/prebuilt/assets/SearchView-Cs7v3m6K.js.map b/frontend/prebuilt/assets/SearchView-Cs7v3m6K.js.map new file mode 100644 index 00000000..43a11016 Binary files /dev/null and b/frontend/prebuilt/assets/SearchView-Cs7v3m6K.js.map differ diff --git a/frontend/prebuilt/assets/SettingsModal-C-alEUMR.js b/frontend/prebuilt/assets/SettingsModal-C-alEUMR.js new file mode 100644 index 00000000..c5ca5478 Binary files /dev/null and b/frontend/prebuilt/assets/SettingsModal-C-alEUMR.js differ diff --git a/frontend/prebuilt/assets/SettingsModal-C-alEUMR.js.map b/frontend/prebuilt/assets/SettingsModal-C-alEUMR.js.map new file mode 100644 index 00000000..77548428 Binary files /dev/null and b/frontend/prebuilt/assets/SettingsModal-C-alEUMR.js.map differ diff --git a/frontend/prebuilt/assets/VisualizerView-BBRdi0S-.js b/frontend/prebuilt/assets/VisualizerView-BBRdi0S-.js new file mode 100644 index 00000000..e187a6a1 Binary files /dev/null and b/frontend/prebuilt/assets/VisualizerView-BBRdi0S-.js differ diff --git a/frontend/prebuilt/assets/VisualizerView-BBRdi0S-.js.map b/frontend/prebuilt/assets/VisualizerView-BBRdi0S-.js.map new file mode 100644 index 00000000..a0697740 Binary files /dev/null and b/frontend/prebuilt/assets/VisualizerView-BBRdi0S-.js.map differ diff --git a/frontend/prebuilt/assets/hooks-NBTgSuxX.js b/frontend/prebuilt/assets/hooks-NBTgSuxX.js new file mode 100644 index 00000000..6720f03b Binary files /dev/null and b/frontend/prebuilt/assets/hooks-NBTgSuxX.js differ diff --git a/frontend/prebuilt/assets/hooks-NBTgSuxX.js.map b/frontend/prebuilt/assets/hooks-NBTgSuxX.js.map new file mode 100644 index 00000000..edc92738 Binary files /dev/null and b/frontend/prebuilt/assets/hooks-NBTgSuxX.js.map differ diff --git a/frontend/prebuilt/assets/index-BJ3L5G2s.js b/frontend/prebuilt/assets/index-BJ3L5G2s.js new file mode 100644 index 00000000..d9be552f Binary files /dev/null and b/frontend/prebuilt/assets/index-BJ3L5G2s.js differ diff --git a/frontend/prebuilt/assets/index-BJ3L5G2s.js.map b/frontend/prebuilt/assets/index-BJ3L5G2s.js.map new file mode 100644 index 00000000..cc0130d2 Binary files /dev/null and b/frontend/prebuilt/assets/index-BJ3L5G2s.js.map differ diff --git a/frontend/prebuilt/assets/index-Kkb-Bio9.css b/frontend/prebuilt/assets/index-Kkb-Bio9.css new file mode 100644 index 00000000..8421d558 Binary files /dev/null and b/frontend/prebuilt/assets/index-Kkb-Bio9.css differ diff --git a/frontend/prebuilt/assets/leaflet-Dgihpmma.css b/frontend/prebuilt/assets/leaflet-Dgihpmma.css new file mode 100644 index 00000000..62b00a01 Binary files /dev/null and b/frontend/prebuilt/assets/leaflet-Dgihpmma.css differ diff --git a/frontend/prebuilt/assets/leaflet-TNk2YzRY.js b/frontend/prebuilt/assets/leaflet-TNk2YzRY.js new file mode 100644 index 00000000..56bd547b Binary files /dev/null and b/frontend/prebuilt/assets/leaflet-TNk2YzRY.js differ diff --git a/frontend/prebuilt/assets/leaflet-TNk2YzRY.js.map b/frontend/prebuilt/assets/leaflet-TNk2YzRY.js.map new file mode 100644 index 00000000..10f8bed4 Binary files /dev/null and b/frontend/prebuilt/assets/leaflet-TNk2YzRY.js.map differ diff --git a/frontend/prebuilt/assets/separator-ChUIbJIJ.js b/frontend/prebuilt/assets/separator-ChUIbJIJ.js new file mode 100644 index 00000000..6229c01a Binary files /dev/null and b/frontend/prebuilt/assets/separator-ChUIbJIJ.js differ diff --git a/frontend/prebuilt/assets/separator-ChUIbJIJ.js.map b/frontend/prebuilt/assets/separator-ChUIbJIJ.js.map new file mode 100644 index 00000000..d6c89661 Binary files /dev/null and b/frontend/prebuilt/assets/separator-ChUIbJIJ.js.map differ diff --git a/frontend/prebuilt/assets/wordlist-BtmChKSf.js b/frontend/prebuilt/assets/wordlist-BtmChKSf.js new file mode 100644 index 00000000..142d5ed2 Binary files /dev/null and b/frontend/prebuilt/assets/wordlist-BtmChKSf.js differ diff --git a/frontend/prebuilt/assets/wordlist-BtmChKSf.js.map b/frontend/prebuilt/assets/wordlist-BtmChKSf.js.map new file mode 100644 index 00000000..309c844c Binary files /dev/null and b/frontend/prebuilt/assets/wordlist-BtmChKSf.js.map differ diff --git a/frontend/prebuilt/favicon-256x256.png b/frontend/prebuilt/favicon-256x256.png new file mode 100644 index 00000000..c7ca86c6 Binary files /dev/null and b/frontend/prebuilt/favicon-256x256.png differ diff --git a/frontend/prebuilt/favicon-96x96.png b/frontend/prebuilt/favicon-96x96.png new file mode 100644 index 00000000..d8b794f7 Binary files /dev/null and b/frontend/prebuilt/favicon-96x96.png differ diff --git a/frontend/prebuilt/favicon.ico b/frontend/prebuilt/favicon.ico new file mode 100644 index 00000000..66ce0b3f Binary files /dev/null and b/frontend/prebuilt/favicon.ico differ diff --git a/frontend/prebuilt/favicon.svg b/frontend/prebuilt/favicon.svg new file mode 100644 index 00000000..fdf37c39 Binary files /dev/null and b/frontend/prebuilt/favicon.svg differ diff --git a/frontend/prebuilt/glyph.png b/frontend/prebuilt/glyph.png new file mode 100644 index 00000000..99fa871a Binary files /dev/null and b/frontend/prebuilt/glyph.png differ diff --git a/frontend/prebuilt/glyph.svg b/frontend/prebuilt/glyph.svg new file mode 100644 index 00000000..e66f46e5 Binary files /dev/null and b/frontend/prebuilt/glyph.svg differ diff --git a/frontend/prebuilt/index.html b/frontend/prebuilt/index.html new file mode 100644 index 00000000..3ba8dfae Binary files /dev/null and b/frontend/prebuilt/index.html differ diff --git a/frontend/prebuilt/robots.txt b/frontend/prebuilt/robots.txt new file mode 100644 index 00000000..1f53798b Binary files /dev/null and b/frontend/prebuilt/robots.txt differ diff --git a/frontend/prebuilt/web-app-manifest-192x192.png b/frontend/prebuilt/web-app-manifest-192x192.png new file mode 100644 index 00000000..a8820761 Binary files /dev/null and b/frontend/prebuilt/web-app-manifest-192x192.png differ diff --git a/frontend/prebuilt/web-app-manifest-512x512.png b/frontend/prebuilt/web-app-manifest-512x512.png new file mode 100644 index 00000000..c7fd26df Binary files /dev/null and b/frontend/prebuilt/web-app-manifest-512x512.png differ diff --git a/tests/test_frontend_static.py b/tests/test_frontend_static.py index fbc21cec..212b85c1 100644 --- a/tests/test_frontend_static.py +++ b/tests/test_frontend_static.py @@ -5,8 +5,10 @@ from fastapi.testclient import TestClient from app.frontend_static import ( ASSET_CACHE_CONTROL, + FRONTEND_BUILD_INSTRUCTIONS, INDEX_CACHE_CONTROL, STATIC_FILE_CACHE_CONTROL, + register_first_available_frontend_static_routes, register_frontend_missing_fallback, register_frontend_static_routes, ) @@ -28,8 +30,7 @@ def test_missing_dist_logs_error_and_keeps_app_running(tmp_path, caplog): with TestClient(app) as client: resp = client.get("/") assert resp.status_code == 404 - assert "npm install" in resp.json()["detail"] - assert "npm run build" in resp.json()["detail"] + assert FRONTEND_BUILD_INSTRUCTIONS in resp.json()["detail"] def test_missing_index_logs_error_and_skips_frontend_routes(tmp_path, caplog): @@ -120,3 +121,41 @@ def test_webmanifest_uses_forwarded_origin_headers(tmp_path): assert data["start_url"] == "https://mesh.example.com:8443/" assert data["scope"] == "https://mesh.example.com:8443/" assert data["id"] == "https://mesh.example.com:8443/" + + +def test_first_available_prefers_dist_over_prebuilt(tmp_path): + app = FastAPI() + frontend_dir = tmp_path / "frontend" + dist_dir = frontend_dir / "dist" + prebuilt_dir = frontend_dir / "prebuilt" + dist_dir.mkdir(parents=True) + prebuilt_dir.mkdir(parents=True) + (dist_dir / "index.html").write_text("dist") + (prebuilt_dir / "index.html").write_text("prebuilt") + + selected = register_first_available_frontend_static_routes(app, [dist_dir, prebuilt_dir]) + + assert selected == dist_dir.resolve() + + with TestClient(app) as client: + response = client.get("/") + assert response.status_code == 200 + assert "dist" in response.text + + +def test_first_available_uses_prebuilt_when_dist_missing(tmp_path): + app = FastAPI() + frontend_dir = tmp_path / "frontend" + dist_dir = frontend_dir / "dist" + prebuilt_dir = frontend_dir / "prebuilt" + prebuilt_dir.mkdir(parents=True) + (prebuilt_dir / "index.html").write_text("prebuilt") + + selected = register_first_available_frontend_static_routes(app, [dist_dir, prebuilt_dir]) + + assert selected == prebuilt_dir.resolve() + + with TestClient(app) as client: + response = client.get("/") + assert response.status_code == 200 + assert "prebuilt" in response.text