150 lines
4.9 KiB
Python
150 lines
4.9 KiB
Python
"""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
|