Files
gao 8f016f25df chore: baseline — ORM migration + systemd deploy + MCP server + daily check
- ORM migration: models/ops 重构
- deploy: 标准化 systemd timer/service 部署体系
- MCP server: 5 个只读工具(任务/状态/数据集)
- daily check: 数据一致性巡检
- watch: task_watch 后台监控
- kline_daily: 麦蕊优先,时间门禁15:00
- 删除: task_industry_sector, task_sector_features
2026-07-21 16:37:00 +08:00

134 lines
4.8 KiB
Python

"""看板 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,
MarketRegimeDaily,
LonghubangDaily, LonghubangSeat, Stock,
NodeCategory, Node, StockNodeMap, KlineStockMADaily,
KlineStockMACDDaily, KlineStockKDJDaily, KlineStockBOLLDaily,
)
from app.core.db.orm import SessionLocal
targets = [
("stocks", Stock),
("kline_stock", KlineStock),
("kline_index", KlineIndex),
("kline_5min", Kline5Min),
("kline_stock_ma_daily", KlineStockMADaily),
("moneyflow", Moneyflow),
("share", Share),
("tick_trade", TickTrade),
("market_regime_daily", MarketRegimeDaily),
("longhubang_daily", LonghubangDaily),
("longhubang_seat", LonghubangSeat),
("node_categories", NodeCategory),
("nodes", Node),
("stock_node_map", StockNodeMap),
("kline_stock_macd_daily", KlineStockMACDDaily),
("kline_stock_kdj_daily", KlineStockKDJDaily),
("kline_stock_boll_daily", KlineStockBOLLDaily),
]
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")