"""看板 API:每天同步情况 / 任务历史 / 数据集统计。 复用 db_ops.list_sync_history() + daily_sync_summary()。 """ from __future__ import annotations from datetime import date from fastapi import APIRouter, Query from app.core.db import ops as db_ops router = APIRouter(prefix="/api/dashboard", tags=["dashboard"]) @router.get("/daily") def daily_summary(days: int = Query(7, ge=1, le=90)): """按日期聚合的同步状态计数(看板首页用)。""" return {"days": days, "summary": db_ops.daily_sync_summary(days=days)} @router.get("/history") def sync_history( dataset_id: str | None = Query(None), days: int = Query(7, ge=1, le=90), limit: int = Query(200, ge=1, le=1000), run_date: str | None = Query(None, description="YYYY-MM-DD 过滤某天"), ): """同步历史记录(每次 run 一行,append-only)。""" rd = None if run_date: try: rd = date.fromisoformat(run_date) except ValueError: return {"error": f"run_date must be YYYY-MM-DD, got {run_date!r}"} rows = db_ops.list_sync_history( dataset_id=dataset_id, run_date=rd, days=days, limit=limit, ) return {"dataset_id": dataset_id, "days": days, "count": len(rows), "history": rows} @router.get("/today") def today_summary(): """今天的同步情况(看板头部卡片用)。""" rows = db_ops.daily_sync_summary(days=1) today_str = date.today().isoformat() today = next((r for r in rows if r["date"] == today_str), None) or { "date": today_str, "ok": 0, "warning": 0, "error": 0, "blocked": 0, "total": 0, "tasks_run": 0, } # 列出今天每个 task 的最新状态 history = db_ops.list_sync_history(days=1, limit=500) return {"summary": today, "tasks": history} @router.get("/datasets") def dataset_overview(): """所有 dataset 的当前状态 + 最近一次 run。 合并 dataset_registry(当前状态)+ sync_history(最近一次历史)。 """ from app.core.sync import get_registry_status registry = get_registry_status() history = db_ops.list_sync_history(days=7, limit=500) last_by_task: dict[str, dict] = {} for h in history: if h["dataset_id"] not in last_by_task: last_by_task[h["dataset_id"]] = h enriched = [] for r in registry: ds_id = r["dataset_id"] last = last_by_task.get(ds_id) enriched.append({ **r, "last_run": last, "history_count_7d": sum(1 for h in history if h["dataset_id"] == ds_id), }) return {"count": len(enriched), "datasets": enriched} @router.get("/stats") def data_stats(): """数据集的数据范围/数据量(用于看板"数据规模"卡片)。""" from sqlalchemy import func, select from app.core.db.models import ( KlineStock, KlineIndex, Kline5Min, Moneyflow, Share, TickTrade, SectorIndices, SectorFeaturesDaily, MarketRegimeDaily, LonghubangDaily, LonghubangSeat, Stocks, Industry, NodeCategory, Node, StockNodeMap, KlineStockMADaily, ) from app.core.db.orm import SessionLocal targets = [ ("stocks", Stocks), ("kline_stock", KlineStock), ("kline_index", KlineIndex), ("kline_5min", Kline5Min), ("kline_stock_ma_daily", KlineStockMADaily), ("moneyflow", Moneyflow), ("share", Share), ("tick_trade", TickTrade), ("sector_indices", SectorIndices), ("sector_features_daily", SectorFeaturesDaily), ("market_regime_daily", MarketRegimeDaily), ("longhubang_daily", LonghubangDaily), ("longhubang_seat", LonghubangSeat), ("industry", Industry), ("node_categories", NodeCategory), ("nodes", Node), ("stock_node_map", StockNodeMap), ] out = [] with SessionLocal() as s: for name, model in targets: try: count = s.execute(select(func.count()).select_from(model)).scalar() or 0 # 尝试拿最早/最新 trade_date date_range = None if hasattr(model, "trade_date"): mn = s.execute(select(func.min(model.trade_date))).scalar() mx = s.execute(select(func.max(model.trade_date))).scalar() date_range = {"min": str(mn), "max": str(mx)} if mn else None out.append({"table": name, "rows": int(count), "date_range": date_range}) except Exception as e: out.append({"table": name, "rows": -1, "error": str(e)[:100]}) return {"tables": out} @router.get("/") def dashboard_index(): """看板 HTML 入口(前端单文件 SPA)。""" from fastapi.responses import FileResponse, RedirectResponse return RedirectResponse(url="/static/dashboard.html")