from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from auth import require_admin_dep from db import get_db from dialogue_store import StoreError, get_conversation, inventory, list_conversations, list_derived_for_conversation, list_messages from identity_store import ( confirm_identity, confirm_review_proposal, deactivate_identity, delete_identity, delete_review_proposal, dismiss_review_proposal, list_registry, list_review_proposals, update_identity, ) from provider_settings import ROLES, SettingsError, public_status, set_role_key, upsert_setting from version import APP_VERSION, BUILD_DATE, MODULE_VERSIONS router = APIRouter(prefix="/api/admin", tags=["admin"]) @router.get("/health") def admin_health(session: dict = Depends(require_admin_dep)): with get_db() as conn: users = conn.execute("SELECT COUNT(*) AS n FROM profiles").fetchone()["n"] prompts = conn.execute("SELECT COUNT(*) AS n FROM ai_prompts").fetchone()["n"] features = conn.execute("SELECT COUNT(*) AS n FROM features").fetchone()["n"] dialogue = inventory() providers = public_status() return { "ok": True, "service": "kansho", "version": APP_VERSION, "build_date": BUILD_DATE, "modules": MODULE_VERSIONS, "inventory": { "users": users, "prompts": prompts, "features": features, "dialogue": dialogue, "providers": { role["role"]: {"ready": role["ready"], "key_present": role["key_present"]} for role in providers["roles"] }, }, "session": { "profile_id": session["profile_id"], "role": session["role"], }, } @router.get("/dialogue") def admin_dialogue(session: dict = Depends(require_admin_dep)): return { "instance": inventory(), "own_conversations": list_conversations(session["profile_id"]), "note": "Fremde Lived Experience ist kein Alltagspfad. Detailansicht nur für das eigene Profil.", } @router.get("/dialogue/conversations/{conversation_id}") def admin_conversation(conversation_id: str, session: dict = Depends(require_admin_dep)): try: conv = get_conversation(session["profile_id"], conversation_id) return { "conversation": conv, "original_messages": list_messages(session["profile_id"], conversation_id), "derived_records": list_derived_for_conversation(session["profile_id"], conversation_id), } except StoreError as exc: raise HTTPException(status_code=exc.status_code, detail={"code": exc.code, "message": exc.message}) from exc class ProviderUpdate(BaseModel): name: str = "" url: str = "" model: str = "" zdr: bool = True no_train: bool = True key: str | None = Field(default=None, description="Write-only. Never returned.") @router.get("/providers") def admin_providers(session: dict = Depends(require_admin_dep)): return public_status() @router.put("/providers/{role}") def admin_update_provider(role: str, body: ProviderUpdate, session: dict = Depends(require_admin_dep)): if role not in ROLES: raise HTTPException(status_code=404, detail={"code": "unknown_provider_role", "message": "Unbekannte Provider-Rolle."}) try: upsert_setting( role, name=body.name, url=body.url, model=body.model, zdr=body.zdr, no_train=body.no_train, ) if body.key is not None and body.key.strip(): set_role_key(role, body.key) except SettingsError as exc: raise HTTPException(status_code=exc.status_code, detail={"code": exc.code, "message": exc.message}) from exc return public_status() class IdentityWrite(BaseModel): canonical_label: str = "" entity_type: str = "PERSON" aliases: list[str] = Field(default_factory=list) token: str | None = None status: str | None = None class IdentityPatch(BaseModel): canonical_label: str | None = None entity_type: str | None = None aliases: list[str] | None = None status: str | None = None def _identity_error(exc: ValueError) -> HTTPException: code = str(exc) messages = { "empty_label": "Bezeichnung ist leer oder nicht maskierbar.", "invalid_entity_type": "Unzulässiger Entitätstyp.", "invalid_status": "Unzulässiger Status.", "identity_missing": "Registry-Eintrag fehlt.", "proposal_missing": "Vorschlag fehlt.", } return HTTPException(status_code=400, detail={"code": code, "message": messages.get(code, "Ungültige Identitätsangabe.")}) @router.get("/identities") def admin_identities(session: dict = Depends(require_admin_dep)): profile_id = session["profile_id"] return { "registry": list_registry(profile_id), "proposals": list_review_proposals(profile_id, include_dismissed=True), "note": "Nur lokale bestätigte Registry und unbestätigte Vorschläge. Kein externer Egress.", "backup": ( "Vor einer Bereinigung die lokale Datei backend/data/kansho.sqlite kopieren. " "Wiederherstellung: Anwendung beenden und die Kopie zurücklegen." ), } @router.post("/identities") def admin_create_identity(body: IdentityWrite, session: dict = Depends(require_admin_dep)): try: return confirm_identity( session["profile_id"], body.canonical_label, entity_type=body.entity_type, token=body.token, aliases=body.aliases, ) except ValueError as exc: raise _identity_error(exc) from exc @router.patch("/identities/{identity_id}") def admin_update_identity(identity_id: str, body: IdentityPatch, session: dict = Depends(require_admin_dep)): try: return update_identity( session["profile_id"], identity_id, canonical_label=body.canonical_label, entity_type=body.entity_type, aliases=body.aliases, status=body.status, ) except ValueError as exc: raise _identity_error(exc) from exc @router.post("/identities/{identity_id}/deactivate") def admin_deactivate_identity(identity_id: str, session: dict = Depends(require_admin_dep)): try: return deactivate_identity(session["profile_id"], identity_id) except ValueError as exc: raise _identity_error(exc) from exc @router.delete("/identities/{identity_id}") def admin_delete_identity(identity_id: str, session: dict = Depends(require_admin_dep)): delete_identity(session["profile_id"], identity_id) return {"ok": True} @router.post("/identities/proposals/{proposal_id}/confirm") def admin_confirm_proposal(proposal_id: str, body: IdentityPatch | None = None, session: dict = Depends(require_admin_dep)): payload = body or IdentityPatch() try: return confirm_review_proposal( session["profile_id"], proposal_id, canonical_label=payload.canonical_label, entity_type=payload.entity_type, aliases=payload.aliases, ) except ValueError as exc: raise _identity_error(exc) from exc @router.post("/identities/proposals/{proposal_id}/dismiss") def admin_dismiss_proposal(proposal_id: str, session: dict = Depends(require_admin_dep)): dismiss_review_proposal(session["profile_id"], proposal_id) return {"ok": True} @router.delete("/identities/proposals/{proposal_id}") def admin_delete_proposal(proposal_id: str, session: dict = Depends(require_admin_dep)): delete_review_proposal(session["profile_id"], proposal_id) return {"ok": True}