feat(dashboard): add Eastern-time activity overview and submission calendar

This commit is contained in:
yuxuanhui
2026-09-12 00:04:57 +08:00
parent 5d7c31810e
commit 9372c47580
9 changed files with 1203 additions and 449 deletions
+74 -2
View File
@@ -1,8 +1,9 @@
"""Read-only Pyramid dashboard with an allowlisted, stable browser contract."""
"""Read-only platform dashboard with an allowlisted, stable browser contract."""
import math
from datetime import date, datetime, timezone
from datetime import date, datetime, timedelta, timezone
from typing import Literal
from zoneinfo import ZoneInfo
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
@@ -12,6 +13,77 @@ from .security import require_auth
from .worldquant import WqError
router = APIRouter(prefix="/api/v1/dashboard", tags=["dashboard"], dependencies=[Depends(require_auth)])
EASTERN = ZoneInfo("America/New_York")
class ActivityDay(BaseModel):
date: date
count: int | None
class ActivitySnapshot(BaseModel):
items: list[ActivityDay]
today: date
timezone: str = "America/New_York"
fetched_at: datetime
def activity_snapshot(data, days: int, at: datetime) -> ActivitySnapshot:
"""Return platform calendar counts, keeping absent dates unknown.
BRAIN activity dates already denote Eastern calendar days. Never interpret
them as UTC instants or replace missing records with zero. Malformed or
duplicate rows raise WqError rather than displaying misleading totals.
"""
today = at.astimezone(EASTERN).date()
start = today - timedelta(days=days - 1)
recordset = data.get("records") if isinstance(data, dict) else None
schema = recordset.get("schema") if isinstance(recordset, dict) else None
properties = schema.get("properties") if isinstance(schema, dict) else None
records = recordset.get("records") if isinstance(recordset, dict) else None
if not isinstance(properties, list) or not isinstance(records, list):
raise WqError("平台活动数据格式无法识别", "invalid_response")
names = [prop.get("name") if isinstance(prop, dict) else prop for prop in properties]
if names.count("date") != 1 or names.count("value") != 1:
raise WqError("平台活动数据格式无法识别", "invalid_response")
date_index, value_index = names.index("date"), names.index("value")
counts = {}
for row in records:
if not isinstance(row, list) or len(row) <= max(date_index, value_index):
raise WqError("平台活动数据格式无法识别", "invalid_response")
raw_date, value = row[date_index], row[value_index]
try:
day = date.fromisoformat(raw_date)
if day.isoformat() != raw_date:
raise ValueError
except (TypeError, ValueError):
raise WqError("平台活动日期格式无法识别", "invalid_response") from None
if day in counts or (value is not None and (type(value) is not int or value < 0)):
raise WqError("平台活动次数格式无法识别", "invalid_response")
counts[day] = value
return ActivitySnapshot(
items=[ActivityDay(date=day, count=counts.get(day))
for offset in range(days) for day in [start + timedelta(days=offset)]],
today=today, fetched_at=at,
)
@router.get("/activity/{metric}", response_model=ActivitySnapshot)
async def activity(request: Request, metric: Literal["submissions", "simulations"]):
"""Read each activity independently using the bound platform session."""
runner = request.app.state.runner
async with request.app.state.sessions() as db:
account = await db.get(Account, 1)
if runner.disconnecting or account.connection_status != "connected" or not account.wq_user_id:
raise HTTPException(409, "请先在个人信息页连接并确认 WorldQuant 账户")
try:
data = await runner.client.get(f"/users/self/activities/{metric}")
return activity_snapshot(data, 182 if metric == "submissions" else 14, datetime.now(timezone.utc))
except WqError as exc:
raise HTTPException(
409 if exc.code in ("disconnected", "verification_required", "authentication_failed") else 502,
str(exc),
) from None
class PyramidRow(BaseModel):