"""API des Viewers (Phase 2). Lese-Routen über viewer.py, Lauf-Routen über scan_dienst.""" from pathlib import Path from fastapi import APIRouter, HTTPException import agents import database as db import projekte import scan_dienst import viewer router = APIRouter(prefix="/api") def _wurzel(name: str) -> Path: wurzel = projekte.finde(name) if wurzel is None: raise HTTPException(404, f"Projekt '{name}' nicht in projekte.txt") return wurzel @router.get("/health") async def health(): return {"ok": True} @router.get("/fs") async def fs(pfad: str = ""): """Ordner-Browser für die Projekt-Auswahl: nur Verzeichnisse, keine versteckten.""" basis = Path(pfad).expanduser() if pfad else Path.home() if not basis.is_dir(): raise HTTPException(404, f"{basis} ist kein Verzeichnis") basis = basis.resolve() try: kinder = sorted(basis.iterdir(), key=lambda p: p.name.lower()) except PermissionError: raise HTTPException(403, f"kein Zugriff auf {basis}") ordner = [{"name": k.name, "pfad": str(k), "hat_planer": (k / ".planer").is_dir()} for k in kinder if k.is_dir() and not k.name.startswith(".")] eltern = str(basis.parent) if basis.parent != basis else None return {"pfad": str(basis), "eltern": eltern, "ordner": ordner} @router.post("/projekte") async def projekt_hinzufuegen(body: dict): pfad = (body.get("pfad") or "").strip() if not pfad: raise HTTPException(400, "pfad fehlt") try: projekte.hinzufuegen(pfad) except ValueError as e: raise HTTPException(409, str(e)) return {"ok": True} @router.delete("/projekte/{name}") async def projekt_entfernen(name: str): lauf = scan_dienst.status(name) if lauf and lauf["laeuft"]: raise HTTPException(409, "Scan läuft — erst abwarten oder abbrechen") try: projekte.entfernen(name) except ValueError as e: raise HTTPException(404, str(e)) return {"ok": True} @router.get("/projekte") async def liste(): out = [] for p in projekte.lade(): eintrag = dict(p) if "fehler" not in p: wurzel = Path(p["pfad"]) eintrag["hat_planer"] = (wurzel / ".planer" / "kern.md").is_file() staende = {d: s for d, (s, _) in scan_dienst.einheiten_bestand(wurzel).items()} eintrag["drift"] = viewer.drift(wurzel, staende)["veraltet"] eintrag["letzter_scan_ts"] = await db.letzter_agent_ts(p["name"]) lauf = scan_dienst.status(p["name"]) eintrag["scan_laeuft"] = bool(lauf and lauf["laeuft"]) out.append(eintrag) return out @router.get("/projekte/{name}") async def projekt(name: str): wurzel = _wurzel(name) daten = viewer.lade_projekt(wurzel) einheiten_kurz = {eid: {"beschreibung": e["beschreibung"]} for eid, e in daten["einheiten"].items()} return { "name": name, "kern": daten["kern"], "bereiche": daten["bereiche"], "architektur": daten["architektur"], "flows": daten["flows"], "pruefung": daten["pruefung"], "einheiten": einheiten_kurz, "doku_ordnung": viewer.doku_ordnung(daten), "drift": viewer.drift(wurzel, daten["staende"]), } @router.get("/projekte/{name}/einheit") async def einheit(name: str, id: str): wurzel = _wurzel(name) daten = viewer.lade_projekt(wurzel) e = daten["einheiten"].get(id) if e is None: raise HTTPException(404, f"Einheit '{id}' unbekannt") quelldatei = id.partition("::")[0] veraltet = quelldatei in viewer.drift(wurzel, daten["staende"])["veraltet"] code = viewer.code_extrahieren(wurzel, id) for anker in e["anker"]: zusatz = viewer.code_extrahieren(wurzel, anker) if zusatz and zusatz not in code: code += ("\n\n" if code else "") + zusatz return {**e, "code": code, "veraltet": veraltet} @router.post("/projekte/{name}/scan") async def scan_starten(name: str, body: dict): wurzel = _wurzel(name) art = body.get("art", "scan") if art not in ("scan", "rescan"): raise HTTPException(400, f"unbekannte Art '{art}'") try: status = scan_dienst.starte(name, wurzel, art) except scan_dienst.VorflugFehler as e: raise HTTPException(409, str(e)) return {"status": status} @router.get("/projekte/{name}/scan-status") async def scan_status(name: str): _wurzel(name) lauf = scan_dienst.status(name) if lauf is None: return {"laeuft": False, "art": None, "ergebnis": None, "stufen": {}, "agenten": [], "dead": []} out = dict(lauf) if lauf["laeuft"]: out["stufen"] = (await db.kanban_stage_counts(name)).get("scan", {}) out["agenten"] = agents.active_agents(f"scan-{name}-") out["dead"] = [c["card_id"] for c in await db.kanban_dead(name)] else: out["stufen"] = {} out["agenten"] = [] out["dead"] = (lauf["ergebnis"] or {}).get("dead", []) return out