"""Small internal operational API. These endpoints are intentionally read-only and are used by future HTMX/API partials and external monitoring. They do not replace the current admin pages. """ from __future__ import annotations from hmac import compare_digest from fastapi import APIRouter, Body, Depends, HTTPException, Request from sqlalchemy import text from app.config import is_production_like_env, settings from app.db import engine def require_internal_access(request: Request) -> None: expected = (settings.clientflow_admin_token or "").strip() if not expected: if is_production_like_env(): raise HTTPException(status_code=503, detail="internal api auth not configured") return received = ( request.headers.get("X-ClientFlow-Admin-Token") or request.cookies.get("clientflow_admin_token") or (request.query_params.get("admin_token") if not is_production_like_env() else None) or "" ).strip() if not received or not compare_digest(received, expected): raise HTTPException(status_code=401, detail="internal api auth required") router = APIRouter(prefix="/api/internal", tags=["internal"], dependencies=[Depends(require_internal_access)]) @router.get("/health") def internal_health() -> dict: return { "status": "ok", "app": settings.app_name, "env": settings.env, "jasmin_enabled": bool(settings.jasmin_enabled), "packlink_enabled": bool(settings.packlink_enabled), } @router.post("/tasks/{task_id}/reply-draft") def internal_task_reply_draft(task_id: str, payload: dict | None = Body(default=None)) -> dict: """Generate a safe editable reply draft for a task. Internal API counterpart of the admin UI button. It persists a draft in message_drafts, returns the suggested message and never sends it. """ payload = payload or {} try: from app.reply_assistant_service import generate_reply_draft state = generate_reply_draft( task_id, template_code=str(payload.get("template_code") or "").strip() or None, selected_document_ids=payload.get("selected_document_ids") or [], persist=True, ) return { "draft_id": state.get("draft_id"), "task_id": task_id, "template_code": (state.get("template") or {}).get("code"), "message_body": state.get("message_body"), "blockers": state.get("blockers") or [], "warnings": state.get("warnings") or [], "email_agent": state.get("email_agent") or {}, "intent_gate": state.get("intent_gate") or {}, "business_knowledge": state.get("business_knowledge") or {}, } except Exception as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc @router.get("/outbox/summary") def outbox_summary() -> dict: with engine.begin() as conn: rows = conn.execute(text(""" SELECT target_system, status, count(*)::int AS total FROM integration_outbox GROUP BY target_system, status ORDER BY target_system, status """)).mappings().all() summary: dict[str, dict[str, int]] = {} for row in rows: target = row["target_system"] or "unknown" status = row["status"] or "unknown" summary.setdefault(target, {})[status] = row["total"] return {"items": summary} @router.get("/documents/summary") def documents_summary() -> dict: with engine.begin() as conn: rows = conn.execute(text(""" SELECT document_kind, status, count(*)::int AS total FROM commercial_documents GROUP BY document_kind, status ORDER BY document_kind, status """)).mappings().all() summary: dict[str, dict[str, int]] = {} for row in rows: kind = row["document_kind"] or "unknown" status = row["status"] or "unknown" summary.setdefault(kind, {})[status] = row["total"] return {"items": summary} @router.get("/operations/summary") def operations_summary() -> dict: from app.operations_service import get_operations_summary return get_operations_summary(limit=10) @router.get("/system/health") def system_health_summary() -> dict: from app.operations_service import get_system_health_summary return get_system_health_summary() @router.get("/communications/summary") def communications_summary() -> dict: from app.communication_service import get_communications_summary return get_communications_summary() @router.get("/communications/recent") def communications_recent(limit: int = 20) -> dict: from app.communication_service import list_communications return {"items": list_communications(limit=limit)} @router.get("/forecast") @router.get("/revenue-forecast") def revenue_forecast(limit: int = 1000, month: str | None = None, metric: str = "invoiced") -> dict: from app.revenue_forecast_service import get_revenue_forecast return get_revenue_forecast(limit=limit, month=month, metric=metric)