docs: add Agent API docs, OpenAPI field descriptions, and project context files

This commit is contained in:
Frank Schwenk
2026-09-22 14:57:16 +02:00
parent 88bdf6f34b
commit 644b2304b3
12 changed files with 1451 additions and 25 deletions
+27 -6
View File
@@ -23,6 +23,7 @@ NO_STORE = {"Cache-Control": "no-store"}
@router.get("/health")
async def agent_health(_: dict = Depends(require_agent)) -> JSONResponse:
"""Liveness-Check der App."""
db = await get_db()
try:
db_version = await current_version(db)
@@ -36,6 +37,7 @@ async def agent_health(_: dict = Depends(require_agent)) -> JSONResponse:
@router.get("/status")
async def agent_status(_: dict = Depends(require_agent)) -> JSONResponse:
"""Heutige Slot-Daten plus aktuelle Stats."""
config = load_meds_config()
db = await get_db()
try:
@@ -47,7 +49,10 @@ async def agent_status(_: dict = Depends(require_agent)) -> JSONResponse:
@router.post("/log")
async def agent_log(payload: AgentLogEntry, _: dict = Depends(require_agent)) -> JSONResponse:
async def agent_log(
payload: AgentLogEntry, _: dict = Depends(require_agent)
) -> JSONResponse:
"""Eine Einnahme für einen Slot loggen."""
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")
@@ -58,23 +63,35 @@ async def agent_log(payload: AgentLogEntry, _: dict = Depends(require_agent)) ->
db = await get_db()
try:
await db.execute("DELETE FROM intake_log WHERE slot_id = ? AND day = ?", (payload.slot_id, day))
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.execute(
"DELETE FROM snooze WHERE slot_id = ? AND day = ?",
(payload.slot_id, day),
)
await db.commit()
finally:
await db.close()
logger.info("Agent logged slot=%s status=%s day=%s", payload.slot_id, status, day)
return JSONResponse({"ok": True, "slot_id": payload.slot_id, "day": day, "status": status}, headers=NO_STORE)
return JSONResponse(
{"ok": True, "slot_id": payload.slot_id, "day": day, "status": status},
headers=NO_STORE,
)
@router.get("/history")
async def agent_history(days: int = 90, _: dict = Depends(require_agent)) -> JSONResponse:
async def agent_history(
days: int = 90, _: dict = Depends(require_agent)
) -> JSONResponse:
"""Einnahme-Historie der letzten N Tage (max. 365)."""
db = await get_db()
try:
data = await build_history(db, days=min(days, 365))
@@ -85,6 +102,7 @@ async def agent_history(days: int = 90, _: dict = Depends(require_agent)) -> JSO
@router.get("/config")
async def agent_config(_: dict = Depends(require_agent)) -> JSONResponse:
"""Aktive Medikamenten-Konfiguration (Zeitzone, Slots)."""
config = load_meds_config()
return JSONResponse(
{
@@ -97,7 +115,10 @@ async def agent_config(_: dict = Depends(require_agent)) -> JSONResponse:
@router.post("/notify")
async def agent_notify(payload: AgentNotifyRequest, _: dict = Depends(require_agent)) -> JSONResponse:
async def agent_notify(
payload: AgentNotifyRequest, _: dict = Depends(require_agent)
) -> JSONResponse:
"""Push-Benachrichtigung an alle aktiven Subscriptions senden."""
db = await get_db()
try:
sent = await send_push(
+7 -1
View File
@@ -57,7 +57,12 @@ async def lifespan(app: FastAPI):
yield
app = FastAPI(title="TakeYourMeds", version=settings.app_version, lifespan=lifespan)
app = FastAPI(
title="TakeYourMeds",
description="Persönliche Medikamenten-PWA mit Push-Erinnerungen, Einnahme-Logging und Agent-API.",
version=settings.app_version,
lifespan=lifespan,
)
app.add_middleware(CacheControlMiddleware)
# API routers
@@ -70,6 +75,7 @@ app.include_router(stats.router)
app.include_router(health.router)
app.include_router(agent.router)
# Backwards-compatible VAPID endpoint used by older clients
@app.get("/api/vapid-public-key")
async def vapid_public_key_legacy() -> JSONResponse:
+84 -18
View File
@@ -13,20 +13,50 @@ class LogStatus(str, Enum):
class PinLogin(BaseModel):
pin: str = Field(..., min_length=3, max_length=12)
pin: str = Field(
...,
min_length=3,
max_length=12,
description="Login-PIN, wie in APP_PIN hinterlegt.",
)
class LogEntry(BaseModel):
slot_id: str = Field(..., min_length=1, max_length=64)
status: LogStatus = LogStatus.taken
day: date | None = None
logged_at: str | None = None
source: str = "app"
slot_id: str = Field(
...,
min_length=1,
max_length=64,
description="ID des Medikamenten-Slots, z.B. 'morning'.",
)
status: LogStatus = Field(
default=LogStatus.taken,
description="Status der Einnahme: taken, missed oder snoozed.",
)
day: date | None = Field(
default=None,
description="Optional: Tag im ISO-Format (YYYY-MM-DD). Standard ist heute.",
)
logged_at: str | None = Field(
default=None,
description="Optional: Zeitstempel im ISO-Format. Standard ist jetzt (UTC).",
)
source: str = Field(
default="app",
description="Herkunft des Logs, z.B. 'app', 'widget' oder 'voice'.",
)
class SnoozeRequest(BaseModel):
slot_id: str = Field(..., min_length=1, max_length=64)
minutes: int = Field(...)
slot_id: str = Field(
...,
min_length=1,
max_length=64,
description="ID des Slots, der verschoben werden soll.",
)
minutes: int = Field(
...,
description="Snooze-Dauer in Minuten. Erlaubt: 15 oder 30.",
)
@field_validator("minutes")
@classmethod
@@ -37,19 +67,55 @@ class SnoozeRequest(BaseModel):
class PushSubscribe(BaseModel):
subscription: dict
subscription: dict = Field(
...,
description="Web-Push-Subscription-Objekt vom Browser (PushManager.subscribe).",
)
class AgentLogEntry(BaseModel):
slot_id: str = Field(..., min_length=1, max_length=64)
status: LogStatus = LogStatus.taken
day: date | None = None
logged_at: str | None = None
source: str = "agent"
slot_id: str = Field(
...,
min_length=1,
max_length=64,
description="ID des Medikamenten-Slots, z.B. 'morning'.",
)
status: LogStatus = Field(
default=LogStatus.taken,
description="Status der Einnahme: taken, missed oder snoozed.",
)
day: date | None = Field(
default=None,
description="Optional: Tag im ISO-Format (YYYY-MM-DD). Standard ist heute.",
)
logged_at: str | None = Field(
default=None,
description="Optional: Zeitstempel im ISO-Format. Standard ist jetzt (UTC).",
)
source: str = Field(
default="agent",
description="Herkunft des Logs, z.B. 'agent', 'voice' oder 'watch'.",
)
class AgentNotifyRequest(BaseModel):
title: str = Field(..., min_length=1, max_length=120)
body: str = Field(..., min_length=1, max_length=512)
slot_id: str | None = None
tag: str | None = None
title: str = Field(
...,
min_length=1,
max_length=120,
description="Titel der Push-Benachrichtigung.",
)
body: str = Field(
...,
min_length=1,
max_length=512,
description="Textkörper der Push-Benachrichtigung.",
)
slot_id: str | None = Field(
default=None,
description="Optional: zugehörige Slot-ID, wird an den Client weitergegeben.",
)
tag: str | None = Field(
default=None,
description="Optional: Notification-Tag für das Ersetzen älterer Notifications.",
)