import os from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse, JSONResponse, Response from app.core.dashboard import render_dashboard from app.core.platform import MonitoringPlatform platform = MonitoringPlatform() @asynccontextmanager async def lifespan(app): if os.getenv("DISABLE_WORKER", "false").lower() not in ("1", "true", "yes", "on"): platform.start() yield platform.stop() app = FastAPI(title="监控平台", version="1.0.0", lifespan=lifespan) @app.get("/", response_class=HTMLResponse) def dashboard(): return render_dashboard() @app.get("/manifest.webmanifest") def manifest(): return JSONResponse({ "name": "监控平台", "short_name": "监控", "description": "价格、财务指标和脚本健康统一监控", "start_url": "/", "scope": "/", "display": "standalone", "background_color": "#f3f5f8", "theme_color": "#111827", "icons": [ { "src": "/icons/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable", } ], }, media_type="application/manifest+json") @app.get("/service-worker.js") def service_worker(): script = """ const CACHE_NAME = "coinpush-pwa-v1"; const APP_SHELL = ["/", "/manifest.webmanifest", "/icons/icon.svg"]; self.addEventListener("install", (event) => { event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))); self.skipWaiting(); }); self.addEventListener("activate", (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all( keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)) )) ); self.clients.claim(); }); self.addEventListener("fetch", (event) => { const url = new URL(event.request.url); if (url.pathname.startsWith("/api/") || url.pathname === "/healthz") return; if (event.request.mode === "navigate") { event.respondWith( fetch(event.request) .then((response) => { const copy = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put("/", copy)); return response; }) .catch(() => caches.match("/") || new Response("Offline", {status: 503})) ); return; } event.respondWith( caches.match(event.request).then((cached) => cached || fetch(event.request)) ); }); """.strip() return Response(script, media_type="application/javascript") @app.get("/icons/icon.svg") def app_icon(): svg = """ """.strip() return Response(svg, media_type="image/svg+xml") @app.get("/healthz") def healthz(): status = platform.status() errors = [m for m in status["monitors"] if m["status"] == "error"] return { "ok": len(errors) == 0, "started": status["started"], "error_monitors": len(errors), } @app.get("/api/status") def api_status(): return JSONResponse(platform.status()) @app.get("/api/monitors") def api_monitors(): return platform.store.list_monitors() @app.post("/api/monitors/{monitor_id:path}/pause") def api_pause_monitor(monitor_id: str): try: return platform.set_monitor_paused(monitor_id, True) except KeyError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.post("/api/monitors/{monitor_id:path}/resume") def api_resume_monitor(monitor_id: str): try: return platform.set_monitor_paused(monitor_id, False) except KeyError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/api/alerts") def api_alerts(limit: int = 50): return platform.store.recent_alerts(limit) @app.get("/api/events") def api_events(limit: int = 80): return platform.store.recent_events(limit) @app.get("/api/config") def api_get_config(): return platform.get_config() @app.put("/api/config") async def api_update_config(request: Request): try: cfg = await request.json() if not isinstance(cfg, dict) or "coins" not in cfg or "global" not in cfg: raise ValueError("配置必须包含 coins 和 global") return platform.update_config(cfg) except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @app.post("/api/calibrate") def api_calibrate_all(): try: return platform.calibrate_all() except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc @app.post("/api/calibrate/{coin}") def api_calibrate_coin(coin: str): try: return platform.calibrate_coin(coin) except KeyError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc @app.get("/api/crypto/{coin}/klines") def api_crypto_klines(coin: str, interval: str = "1h", limit: int = 120): try: return JSONResponse(platform.get_klines(coin, interval, limit)) except KeyError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc except (TypeError, ValueError) as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc @app.get("/api/market-move/config") def api_get_market_move_config(): return platform.get_market_move_config() @app.put("/api/market-move/config") async def api_update_market_move_config(request: Request): try: cfg = await request.json() if not isinstance(cfg, dict) or "coins" not in cfg: raise ValueError("配置必须包含 coins") return platform.update_market_move_config(cfg) except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc