63e3c8d607
- Each dashboard is now a self-contained folder: manifest.json + index.html - Auto-scan dashboards/ and data/dashboards/ on startup - Export: .tsd (dashboards), .tss (scenes), .tsp (game plugins) - all zip format - Dashboard index.html is complete standalone page (WS + data binding) - Aspect ratio constraints handled in each dashboard's own JS - Removed template-based dashboard rendering in favor of static serve - Import via file upload endpoints, export via direct file download
115 lines
3.4 KiB
Python
115 lines
3.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
from fastapi.responses import HTMLResponse, FileResponse
|
|
|
|
from config.settings import get_config
|
|
from server.api import router as api_router
|
|
from server.websocket import ws_manager
|
|
from server.telemetry.listener import telemetry_listener
|
|
from utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent
|
|
STATIC_DIR = BASE_DIR / "static"
|
|
TEMPLATES_DIR = BASE_DIR / "templates"
|
|
|
|
os.makedirs(BASE_DIR / "logs", exist_ok=True)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
logger.info("=" * 50)
|
|
logger.info(" TurboSu - Racing Telemetry Dashboard")
|
|
logger.info("=" * 50)
|
|
cfg = get_config()
|
|
logger.info("Server config: %s:%d", cfg.get("server_host", "0.0.0.0"), cfg.get("server_port", 9527))
|
|
await telemetry_listener.start()
|
|
logger.info("Telemetry listener auto-started")
|
|
yield
|
|
telemetry_listener.stop()
|
|
logger.info("TurboSu shutdown complete")
|
|
|
|
|
|
app = FastAPI(title="TurboSu", version="1.0.0", lifespan=lifespan)
|
|
|
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
|
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
|
|
|
|
|
app.include_router(api_router)
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def index(request: Request):
|
|
return templates.TemplateResponse("index.html", {"request": request})
|
|
|
|
|
|
@app.get("/dashboard/{theme_id}", response_class=HTMLResponse)
|
|
async def dashboard_render(request: Request, theme_id: str):
|
|
from models.dashboard import dashboard_manager
|
|
index_html = dashboard_manager.get_index_html(theme_id)
|
|
if index_html:
|
|
return HTMLResponse(content=index_html)
|
|
theme = dashboard_manager.get(theme_id)
|
|
if not theme:
|
|
raise HTTPException(status_code=404, detail="Dashboard not found")
|
|
return HTMLResponse(content=f"""
|
|
<!DOCTYPE html><html><body style="background:#000;color:#888;display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;">
|
|
仪表盘 "{theme['name']}" 缺少 index.html
|
|
</body></html>
|
|
""")
|
|
|
|
|
|
@app.get("/scene/{scene_id}", response_class=HTMLResponse)
|
|
async def scene_render(request: Request, scene_id: str):
|
|
from models.scene import scene_manager
|
|
scene = scene_manager.get(scene_id)
|
|
return templates.TemplateResponse("scene.html", {
|
|
"request": request,
|
|
"scene": scene,
|
|
})
|
|
|
|
|
|
@app.websocket("/ws")
|
|
async def websocket_endpoint(ws: WebSocket):
|
|
cid = await ws_manager.connect(ws)
|
|
try:
|
|
await ws.send_json({"type": "connected", "client_id": cid})
|
|
while True:
|
|
msg = await ws.receive_json()
|
|
if msg.get("type") == "ping":
|
|
await ws.send_json({"type": "pong"})
|
|
except WebSocketDisconnect:
|
|
pass
|
|
except Exception as e:
|
|
logger.error("WS error: %s", e)
|
|
finally:
|
|
ws_manager.disconnect(cid)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
cfg = get_config()
|
|
uvicorn.run(
|
|
"app:app",
|
|
host=cfg.get("server_host", "0.0.0.0"),
|
|
port=cfg.get("server_port", 9527),
|
|
reload=False,
|
|
log_level="info",
|
|
)
|