Files
Frank Schwenk 88bdf6f34b feat: overhaul with modular API, agent endpoints, migrations, backup and tests
- Split monolithic app.py into server/api/ routers
- Add Pydantic schemas for input validation
- Add /agent/* API with separate AGENT_TOKEN auth
- Replace static schema.sql with versioned migrations (datenerhaltend)
- Add tools/backup_db.py for SQLite backups
- Add Dockerfile HEALTHCHECK and compose.yml AGENT_TOKEN env
- Add pytest tests for user and agent API
- Update README with API docs and deployment safety notes
- Bump version to 1.2.0
2026-09-21 12:46:44 +02:00

93 lines
3.1 KiB
Python

from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from server.auth import require_auth
from server.config_loader import load_meds_config, today_str
from server.db import get_db, new_id, utc_now_iso
from server.messages import pick
from server.scheduler import schedule_snooze
from server.schemas import LogEntry, SnoozeRequest
from server.slots import build_today
from server.stats import MILESTONE_DEFS, build_history, check_milestones, compute_stats
router = APIRouter(prefix="/api", tags=["intake"])
logger = logging.getLogger(__name__)
NO_STORE = {"Cache-Control": "no-store"}
@router.get("/today")
async def get_today(_: dict = Depends(require_auth)) -> JSONResponse:
db = await get_db()
try:
data = await build_today(db)
finally:
await db.close()
return JSONResponse(data, headers=NO_STORE)
@router.post("/log")
async def post_log(payload: LogEntry, _: dict = Depends(require_auth)) -> JSONResponse:
config = load_meds_config()
if payload.slot_id not in {s.id for s in config.slots}:
raise HTTPException(status_code=400, detail="Unknown slot")
day = payload.day.isoformat() if payload.day else today_str(config.timezone)
logged_at = payload.logged_at or utc_now_iso()
status = payload.status.value
db = await get_db()
try:
await db.execute("DELETE FROM intake_log WHERE slot_id = ? AND day = ?", (payload.slot_id, day))
await db.execute(
"INSERT INTO intake_log (id, slot_id, day, status, logged_at, source) VALUES (?, ?, ?, ?, ?, ?)",
(new_id(), payload.slot_id, day, status, logged_at, payload.source),
)
if status == "taken":
await db.execute("DELETE FROM snooze WHERE slot_id = ? AND day = ?", (payload.slot_id, day))
await db.commit()
stats = await compute_stats(db)
new_milestones = await check_milestones(db, stats["streak"])
finally:
await db.close()
message = pick("success") if status == "taken" else pick("missed")
return JSONResponse(
{
"ok": True,
"message": message,
"stats": stats,
"new_milestones": [
{"id": m, "title": MILESTONE_DEFS.get(m, m)} for m in new_milestones
],
},
headers=NO_STORE,
)
@router.get("/history")
async def get_history(days: int = 90, _: dict = Depends(require_auth)) -> JSONResponse:
db = await get_db()
try:
data = await build_history(db, days=min(days, 365))
finally:
await db.close()
return JSONResponse(data, headers=NO_STORE)
@router.post("/snooze")
async def post_snooze(payload: SnoozeRequest, _: dict = Depends(require_auth)) -> JSONResponse:
config = load_meds_config()
if payload.slot_id not in {s.id for s in config.slots}:
raise HTTPException(status_code=400, detail="Unknown slot")
until = await schedule_snooze(payload.slot_id, payload.minutes)
return JSONResponse(
{"ok": True, "snooze_until": until, "message": pick("snooze")},
headers=NO_STORE,
)