import json import logging from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse, PlainTextResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from . import config, montage, pipeline, state logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") @asynccontextmanager async def lifespan(app: FastAPI): config.STORAGE.mkdir(exist_ok=True) yield for name in list(pipeline._laeufe): await pipeline.pausieren(name) app = FastAPI(title="buchwerk", lifespan=lifespan) class ThemaNeu(BaseModel): name: str hinweise: str = "" class Position(BaseModel): abschnitt: str | None = None lesemodus: int | None = None def _pruefen(name: str) -> dict: if not state.name_ok(name) or not (t := state.thema(name)): raise HTTPException(404, "Thema unbekannt") return t @app.get("/api/topics") async def topics(): aus = [] for t in state.themen(): st = state.zustand(t["name"]) aus.append({"name": t["name"], "titel": t["titel"], "status": st["status"], "laeuft": pipeline.laeuft(t["name"]), "stufe": st["stufe"]}) return aus @app.post("/api/topics") async def topic_anlegen(body: ThemaNeu): name = body.name.strip() if not state.name_ok(name): raise HTTPException(400, "Ungültiger Name") try: return state.anlegen(name, body.hinweise) except ValueError as e: raise HTTPException(409, str(e)) @app.delete("/api/topics/{name}") async def topic_loeschen(name: str): _pruefen(name) await pipeline.pausieren(name) state.loeschen(name) return {"ok": True} @app.post("/api/topics/{name}/start") async def start(name: str): _pruefen(name) if state.zustand(name)["status"] == "fertig": raise HTTPException(400, "Lauf ist fertig – erst zurücksetzen") pipeline.starten(name) return {"ok": True} @app.post("/api/topics/{name}/pause") async def pause(name: str): _pruefen(name) await pipeline.pausieren(name) return {"ok": True} @app.post("/api/topics/{name}/reset") async def reset(name: str): _pruefen(name) await pipeline.pausieren(name) state.zuruecksetzen(name) return {"ok": True} @app.get("/api/topics/{name}/state") async def zustand(name: str): t = _pruefen(name) st = state.zustand(name) return {"name": name, "titel": t["titel"], "hinweise": t.get("hinweise", ""), **st, "deutung": st.get("deutung") or state.deutung(name), "ereignisse": st["ereignisse"][-100:]} @app.get("/api/topics/{name}/position") async def position_lesen(name: str): _pruefen(name) return state.position(name) @app.put("/api/topics/{name}/position") async def position_schreiben(name: str, body: Position): _pruefen(name) return state.position_setzen(name, {k: v for k, v in body.model_dump().items() if v is not None}) @app.get("/api/topics/{name}/guide") async def guide(name: str): _pruefen(name) return montage.guide(name) @app.get("/api/topics/{name}/guide.md", response_class=PlainTextResponse) async def guide_md(name: str): _pruefen(name) return montage.markdown(name) @app.get("/api/topics/{name}/buch.pdf") async def buch_pdf(name: str): _pruefen(name) p = state.verzeichnis(name) / "buch" / "buch.pdf" if not p.exists(): raise HTTPException(404, "PDF noch nicht gebaut") return FileResponse(p, media_type="application/pdf", filename=f"{name}.pdf") if config.FRONTEND_DIST.exists(): app.mount("/assets", StaticFiles(directory=config.FRONTEND_DIST / "assets"), name="assets") @app.get("/{pfad:path}") async def spa(pfad: str): return FileResponse(config.FRONTEND_DIST / "index.html")