130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
"""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 fastapi import APIRouter, Body, Depends, HTTPException, Request
|
|
from sqlalchemy import text
|
|
|
|
from app.admin_auth import require_admin_auth
|
|
from app.config import settings
|
|
from app.db import engine
|
|
|
|
def require_internal_access(request: Request) -> None:
|
|
"""Apply API auth (including X-ClientFlow-Admin-Token in token mode)."""
|
|
require_admin_auth(request, area="internal_api")
|
|
|
|
|
|
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)
|