feat: add admin UI routes
Add CRUD routes for configs and subscriptions, force-refresh and logs endpoints, stub Jinja2 templates for test coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+166
-3
@@ -4,11 +4,12 @@ from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from fastapi.responses import Response
|
||||
import uuid
|
||||
from fastapi import FastAPI, Depends, HTTPException, Form, Request
|
||||
from fastapi.responses import Response, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, make_url
|
||||
from sqlalchemy import select, desc, make_url
|
||||
|
||||
from models import Base, Config, Subscription, ExportLog, make_engine, make_session_factory
|
||||
from mihomo import MihomoClient
|
||||
@@ -130,3 +131,165 @@ async def get_config(
|
||||
await db.commit()
|
||||
|
||||
return Response(content=expanded, media_type="application/x-yaml")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Config))
|
||||
configs = result.scalars().all()
|
||||
|
||||
last_logs: dict[int, ExportLog | None] = {}
|
||||
for cfg in configs:
|
||||
log_result = await db.execute(
|
||||
select(ExportLog)
|
||||
.where(ExportLog.config_id == cfg.id)
|
||||
.order_by(desc(ExportLog.fetched_at))
|
||||
.limit(1)
|
||||
)
|
||||
last_logs[cfg.id] = log_result.scalar_one_or_none()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{"request": request, "configs": configs, "last_logs": last_logs},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/configs/new")
|
||||
async def new_config_form(request: Request):
|
||||
return templates.TemplateResponse("config_form.html", {"request": request, "config": None})
|
||||
|
||||
|
||||
@app.post("/configs/new")
|
||||
async def create_config(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
base_yaml: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
config = Config(name=name, token=str(uuid.uuid4()), base_yaml=base_yaml)
|
||||
db.add(config)
|
||||
await db.commit()
|
||||
await write_and_reload_mihomo(db)
|
||||
return RedirectResponse(f"/configs/{config.id}", status_code=303)
|
||||
|
||||
|
||||
@app.get("/configs/{config_id}")
|
||||
async def config_detail(config_id: int, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
config = await db.get(Config, config_id)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
|
||||
result = await db.execute(
|
||||
select(Subscription).where(Subscription.config_id == config_id)
|
||||
)
|
||||
subscriptions = result.scalars().all()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"config_detail.html",
|
||||
{"request": request, "config": config, "subscriptions": subscriptions},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/configs/{config_id}")
|
||||
async def update_config(
|
||||
config_id: int,
|
||||
base_yaml: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
config = await db.get(Config, config_id)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
config.base_yaml = base_yaml
|
||||
config.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await write_and_reload_mihomo(db)
|
||||
return RedirectResponse(f"/configs/{config_id}", status_code=303)
|
||||
|
||||
|
||||
@app.post("/configs/{config_id}/delete")
|
||||
async def delete_config(config_id: int, db: AsyncSession = Depends(get_db)):
|
||||
config = await db.get(Config, config_id)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
await db.delete(config)
|
||||
await db.commit()
|
||||
await write_and_reload_mihomo(db)
|
||||
return RedirectResponse("/", status_code=303)
|
||||
|
||||
|
||||
@app.get("/configs/{config_id}/subscriptions/new")
|
||||
async def new_sub_form(config_id: int, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
config = await db.get(Config, config_id)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
return templates.TemplateResponse(
|
||||
"sub_form.html", {"request": request, "config": config}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/configs/{config_id}/subscriptions/new")
|
||||
async def create_subscription(
|
||||
config_id: int,
|
||||
name: str = Form(...),
|
||||
url: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
config = await db.get(Config, config_id)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
sub = Subscription(config_id=config_id, name=name, url=url)
|
||||
db.add(sub)
|
||||
await db.commit()
|
||||
return RedirectResponse(f"/configs/{config_id}", status_code=303)
|
||||
|
||||
|
||||
@app.post("/subscriptions/{sub_id}/delete")
|
||||
async def delete_subscription(sub_id: int, db: AsyncSession = Depends(get_db)):
|
||||
sub = await db.get(Subscription, sub_id)
|
||||
if not sub:
|
||||
raise HTTPException(status_code=404, detail="Subscription not found")
|
||||
config_id = sub.config_id
|
||||
await db.delete(sub)
|
||||
await db.commit()
|
||||
return RedirectResponse(f"/configs/{config_id}", status_code=303)
|
||||
|
||||
|
||||
@app.get("/configs/{config_id}/logs")
|
||||
async def config_logs(config_id: int, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
config = await db.get(Config, config_id)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
|
||||
result = await db.execute(
|
||||
select(ExportLog)
|
||||
.where(ExportLog.config_id == config_id)
|
||||
.order_by(desc(ExportLog.fetched_at))
|
||||
.limit(50)
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"logs.html", {"request": request, "config": config, "logs": logs}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/configs/{config_id}/refresh")
|
||||
async def force_refresh(config_id: int, db: AsyncSession = Depends(get_db)):
|
||||
config = await db.get(Config, config_id)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
|
||||
result = await db.execute(
|
||||
select(Subscription).where(Subscription.config_id == config_id)
|
||||
)
|
||||
subscriptions = result.scalars().all()
|
||||
|
||||
for sub in subscriptions:
|
||||
try:
|
||||
await mihomo_client.refresh_and_collect(sub.name, timeout=30)
|
||||
sub.last_fetched_at = datetime.utcnow()
|
||||
except Exception as e:
|
||||
logger.error("Force-refresh failed for provider %s: %s", sub.name, e)
|
||||
|
||||
await db.commit()
|
||||
return RedirectResponse(f"/configs/{config_id}", status_code=303)
|
||||
|
||||
Reference in New Issue
Block a user