import json import queue from typing import Any, Dict from flask import Flask, Response, jsonify, render_template, request, stream_with_context from waitress import serve from app_state import AppState from network_manager import NetworkManager from serial_bridge import SerialBroadcaster class WebPortal: def __init__( self, state: AppState, network_manager: NetworkManager, broadcaster: SerialBroadcaster, template_folder: str = "../templates", static_folder: str = "../static", ) -> None: self.state = state self.network_manager = network_manager self.broadcaster = broadcaster self.app = Flask(__name__, template_folder=template_folder, static_folder=static_folder) self._register_routes() def _register_routes(self) -> None: @self.app.route("/") def index() -> str: return render_template("index.html") @self.app.route("/serial") def serial_page() -> str: return render_template("serial.html") @self.app.route("/api/status", methods=["GET"]) def status() -> Response: try: self.network_manager.refresh_state() return jsonify(self.state.snapshot()) except Exception as exc: self.state.update_status("Status update failed", str(exc)) return jsonify(self.state.snapshot()), 503 @self.app.route("/api/scan", methods=["POST", "GET"]) def scan() -> Response: try: ssids = self.network_manager.scan_networks() return jsonify({"ok": True, "ssids": ssids}) except Exception as exc: self.state.update_status("Scan failed", str(exc)) return jsonify({"ok": False, "ssids": [], "message": str(exc)}), 503 @self.app.route("/api/connect", methods=["POST"]) def connect() -> Response: payload: Dict[str, Any] = request.get_json(silent=True) or {} ssid = (payload.get("ssid") or "").strip() password = payload.get("password") or "" ok, message = self.network_manager.connect_to_wifi(ssid, password) if not ok: self.state.update_status("Connect failed", message) try: self.network_manager.start_ap() except Exception: pass return jsonify({"ok": False, "message": message}), 400 self.network_manager.refresh_state() return jsonify({"ok": True, "message": message}) @self.app.route("/events/serial") def serial_events() -> Response: @stream_with_context def generate(): q = self.broadcaster.subscribe() try: yield "retry: 2000\n\n" while True: try: line = q.get(timeout=15) data = json.dumps({"line": line}) yield f"data: {data}\n\n" except queue.Empty: yield ": keepalive\n\n" finally: self.broadcaster.unsubscribe(q) return Response(generate(), mimetype="text/event-stream") def run(self, host: str = "0.0.0.0", port: int = 80) -> None: serve(self.app, host=host, port=port, threads=6)