209 lines
10 KiB
Python
209 lines
10 KiB
Python
from fastapi import APIRouter, Cookie, HTTPException, Request
|
|
from fastapi.responses import HTMLResponse, Response
|
|
|
|
from app.config.system_config import seed_runtime_system_defaults
|
|
from app.db import auth_db
|
|
from app.db import chat_assistant_db
|
|
from app.db.analytics import get_cron_run_logs, get_cron_run_summary, get_pipeline_runs
|
|
from app.db.data_export import build_data_export_bundle
|
|
from app.db.operations_dashboard import get_operations_dashboard
|
|
from app.db.scheduler_db import (
|
|
enqueue_manual_trigger,
|
|
get_job_config,
|
|
get_scheduler_overview,
|
|
list_manual_triggers,
|
|
set_job_enabled,
|
|
set_job_interval,
|
|
)
|
|
from app.db.runtime_config_db import delete_config, get_config, list_configs, set_config
|
|
from app.db.system_logs import get_system_error, get_system_error_stats, list_system_errors
|
|
from app.web.shared import (
|
|
RuntimeConfigRequest,
|
|
SchedulerIntervalRequest,
|
|
SchedulerToggleRequest,
|
|
SchedulerTriggerRequest,
|
|
login_redirect,
|
|
require_admin,
|
|
)
|
|
|
|
def build_router(templates):
|
|
router = APIRouter()
|
|
|
|
@router.get("/admin.html", response_class=HTMLResponse)
|
|
async def admin_page(request: Request, altcoin_session: str = Cookie(default="")):
|
|
if not auth_db.get_user_by_session_token(altcoin_session):
|
|
return login_redirect()
|
|
try:
|
|
require_admin(altcoin_session)
|
|
except HTTPException as e:
|
|
return HTMLResponse(content=f"<meta charset=utf-8><h2>需要管理员权限</h2><p>{e.detail}</p><a href=/app>返回看板</a>", status_code=e.status_code)
|
|
return templates.TemplateResponse(request=request, name="admin.html", context={"show_nav": True, "active_nav": "admin"})
|
|
|
|
@router.get("/api/admin/check")
|
|
async def api_admin_check(altcoin_session: str = Cookie(default="")):
|
|
try:
|
|
user = require_admin(altcoin_session)
|
|
return {"is_admin": True, "email": user.get("email", "")}
|
|
except HTTPException:
|
|
return {"is_admin": False}
|
|
|
|
@router.get("/api/admin/stats")
|
|
async def api_admin_stats(altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return auth_db.get_admin_stats()
|
|
|
|
@router.get("/api/admin/operations-dashboard")
|
|
async def api_admin_operations_dashboard(hours: int = 24, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return get_operations_dashboard(hours=hours)
|
|
|
|
@router.get("/api/admin/users")
|
|
async def api_admin_users(search: str = "", offset: int = 0, limit: int = 50, tab: str = "all", altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return auth_db.get_admin_users(search=search, offset=offset, limit=limit, tab=tab)
|
|
|
|
@router.get("/api/admin/orders")
|
|
async def api_admin_orders(search: str = "", offset: int = 0, limit: int = 50, status: str = "all", altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return auth_db.get_admin_orders(search=search, offset=offset, limit=limit, status=status)
|
|
|
|
@router.get("/api/admin/system-errors")
|
|
async def api_admin_system_errors(
|
|
search: str = "",
|
|
offset: int = 0,
|
|
limit: int = 50,
|
|
level: str = "all",
|
|
source: str = "all",
|
|
hours: int = 168,
|
|
altcoin_session: str = Cookie(default=""),
|
|
):
|
|
require_admin(altcoin_session)
|
|
return list_system_errors(search=search, offset=offset, limit=limit, level=level, source=source, hours=hours)
|
|
|
|
@router.get("/api/admin/system-errors/stats")
|
|
async def api_admin_system_error_stats(hours: int = 24, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return get_system_error_stats(hours=hours)
|
|
|
|
@router.get("/api/admin/system-errors/{log_id}")
|
|
async def api_admin_system_error_detail(log_id: int, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
item = get_system_error(log_id)
|
|
if not item:
|
|
raise HTTPException(status_code=404, detail="日志不存在")
|
|
return item
|
|
|
|
@router.get("/api/admin/cron-runs")
|
|
async def api_admin_cron_runs(limit: int = 80, job_name: str = "", altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return {"items": get_cron_run_logs(limit=limit, job_name=job_name or None)}
|
|
|
|
@router.get("/api/admin/cron-runs/summary")
|
|
async def api_admin_cron_run_summary(hours: int = 24, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return get_cron_run_summary(hours=hours)
|
|
|
|
@router.get("/api/admin/pipeline-runs")
|
|
async def api_admin_pipeline_runs(limit: int = 30, hours: int = 24, offset: int = 0, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return get_pipeline_runs(limit=limit, hours=hours, offset=offset)
|
|
|
|
@router.get("/api/admin/chat-logs/overview")
|
|
async def api_admin_chat_logs_overview(hours: int = 24, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return chat_assistant_db.get_chat_admin_overview(hours=hours)
|
|
|
|
@router.get("/api/admin/chat-logs")
|
|
async def api_admin_chat_logs(
|
|
hours: int = 24,
|
|
intent: str = "all",
|
|
search: str = "",
|
|
offset: int = 0,
|
|
limit: int = 50,
|
|
altcoin_session: str = Cookie(default=""),
|
|
):
|
|
require_admin(altcoin_session)
|
|
return chat_assistant_db.list_chat_admin_questions(hours=hours, intent=intent, search=search, offset=offset, limit=limit)
|
|
|
|
@router.get("/api/admin/data-export")
|
|
async def api_admin_data_export(hours: int = 24, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
filename, content, _manifest = build_data_export_bundle(hours=hours)
|
|
return Response(
|
|
content=content,
|
|
media_type="application/zip",
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|
|
|
|
@router.get("/api/runtime-config")
|
|
async def api_runtime_config(kind: str = "all", altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
if kind in ("all", "system"):
|
|
seed_runtime_system_defaults()
|
|
if kind == "strategy":
|
|
return {"items": list_configs("strategy")}
|
|
if kind == "system":
|
|
return {"items": list_configs("system")}
|
|
return {"items": list_configs("strategy") + list_configs("system")}
|
|
|
|
@router.get("/api/runtime-config/{kind}/{config_key:path}")
|
|
async def api_runtime_config_detail(kind: str, config_key: str, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
if kind not in ("strategy", "system"):
|
|
raise HTTPException(status_code=400, detail="kind must be strategy or system")
|
|
return {"kind": kind, "config_key": config_key, "config": get_config(kind, config_key, default={})}
|
|
|
|
@router.put("/api/runtime-config/{kind}/{config_key:path}")
|
|
async def api_runtime_config_update(kind: str, config_key: str, payload: RuntimeConfigRequest, altcoin_session: str = Cookie(default="")):
|
|
user = require_admin(altcoin_session)
|
|
if kind not in ("strategy", "system"):
|
|
raise HTTPException(status_code=400, detail="kind must be strategy or system")
|
|
config = set_config(kind, config_key, payload.config, description=payload.description, source="admin_page", updated_by=user.get("email", ""))
|
|
return {"ok": True, "kind": kind, "config_key": config_key, "config": config}
|
|
|
|
@router.delete("/api/runtime-config/{kind}/{config_key:path}")
|
|
async def api_runtime_config_delete(kind: str, config_key: str, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
if kind not in ("strategy", "system"):
|
|
raise HTTPException(status_code=400, detail="kind must be strategy or system")
|
|
return {"ok": delete_config(kind, config_key), "kind": kind, "config_key": config_key}
|
|
|
|
@router.get("/api/scheduler/jobs")
|
|
async def api_scheduler_jobs(altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return get_scheduler_overview()
|
|
|
|
@router.post("/api/scheduler/jobs/{job_name}/toggle")
|
|
async def api_scheduler_toggle(job_name: str, payload: SchedulerToggleRequest, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
if not set_job_enabled(job_name, payload.enabled):
|
|
raise HTTPException(status_code=404, detail="任务不存在")
|
|
return {"ok": True, "job_name": job_name, "enabled": payload.enabled}
|
|
|
|
@router.post("/api/scheduler/jobs/{job_name}/interval")
|
|
async def api_scheduler_interval(job_name: str, payload: SchedulerIntervalRequest, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
if payload.every_seconds < 30:
|
|
raise HTTPException(status_code=400, detail="周期不能低于 30 秒")
|
|
if not set_job_interval(job_name, payload.every_seconds):
|
|
raise HTTPException(status_code=404, detail="任务不存在")
|
|
return {"ok": True, "job_name": job_name, "every_seconds": payload.every_seconds}
|
|
|
|
@router.post("/api/scheduler/jobs/{job_name}/trigger")
|
|
async def api_scheduler_trigger(job_name: str, payload: SchedulerTriggerRequest, altcoin_session: str = Cookie(default="")):
|
|
user = require_admin(altcoin_session)
|
|
job = get_job_config(job_name)
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="任务不存在")
|
|
if not job.get("enabled") and not payload.force:
|
|
raise HTTPException(status_code=409, detail="任务已关闭,需要确认后 force=true 才能单次运行")
|
|
trigger_id = enqueue_manual_trigger(job_name, force=payload.force, requested_by=user.get("email", ""))
|
|
return {"ok": True, "job_name": job_name, "trigger_id": trigger_id, "force": payload.force}
|
|
|
|
@router.get("/api/scheduler/triggers")
|
|
async def api_scheduler_triggers(limit: int = 30, altcoin_session: str = Cookie(default="")):
|
|
require_admin(altcoin_session)
|
|
return {"items": list_manual_triggers(limit=limit)}
|
|
|
|
return router
|