from __future__ import annotations import dataclasses import subprocess from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel from app.services import PortScanner from app.tunnel_manager import TunnelManager app = FastAPI(title="Gradio Tunnel Dashboard") manager = TunnelManager() scanner = PortScanner() app.mount("/static", StaticFiles(directory="app/static"), name="static") templates = Jinja2Templates(directory="app/templates") class TunnelRequest(BaseModel): port: int @app.get("/", response_class=HTMLResponse) async def index(request: Request) -> HTMLResponse: return templates.TemplateResponse("index.html", {"request": request}) @app.get("/api/ports") async def list_ports() -> list[dict[str, str | int | None]]: records = scanner.scan() return [dataclasses.asdict(record) for record in sorted(records, key=lambda item: item.port)] @app.get("/api/tunnels") async def list_tunnels() -> list[dict[str, str | int | None]]: tunnels = manager.list_tunnels() return [ { "port": info.port, "command": info.command, "pid": info.pid, "url": info.url, "status": info.status, "exit_code": info.exit_code, "last_output": info.last_output, } for info in tunnels.values() ] @app.get("/api/tunnels/{port}/logs") async def tunnel_logs(port: int) -> dict[str, list[str]]: logs = manager.get_logs(port) if logs is None: raise HTTPException(status_code=404, detail="Tunnel not running.") return {"logs": logs} @app.post("/api/tunnels") async def start_tunnel(payload: TunnelRequest) -> dict[str, str | int | None]: if payload.port <= 0 or payload.port > 65535: raise HTTPException(status_code=400, detail="Invalid port.") info = manager.start_tunnel(payload.port) return { "port": info.port, "command": info.command, "pid": info.pid, "url": info.url, "status": info.status, "exit_code": info.exit_code, "last_output": info.last_output, } @app.delete("/api/tunnels/{port}") async def stop_tunnel(port: int) -> dict[str, bool]: stopped = manager.stop_tunnel(port) if not stopped: raise HTTPException(status_code=404, detail="Tunnel not running.") return {"stopped": True} @app.on_event("shutdown") def shutdown_event() -> None: manager.shutdown()