feat: add autonomous fiscal control panel

This commit is contained in:
gti
2026-09-22 15:36:12 -03:00
parent 636ed5228b
commit 301aac112e
34 changed files with 1252 additions and 9 deletions
+125
View File
@@ -0,0 +1,125 @@
"""Human-authenticated JSON API for the control panel."""
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select
from .audit import record_audit
from .control_plane import dashboard_summary, installation_view, list_blockers, list_installations, list_jobs, list_panel_events
from .human_auth import require_csrf, require_human
from .models import Agent, AgentSnapshot, BlockedProduct, Installation, Tenant
from .jobs import create_job
def router(session_factory):
routes = APIRouter(prefix="/v1")
def identity(request: Request):
session = session_factory()
try:
web_session, admin = require_human(request, session)
return session, web_session, admin
except Exception:
session.close(); raise
@routes.get("/dashboard/summary")
def summary(identity=Depends(identity)):
session, _, _ = identity
try: return dashboard_summary(session)
finally: session.close()
@routes.get("/agents")
def agents(page: int = 1, page_size: int = 50, client: str | None = None, health: str | None = None, version: str | None = None, search: str | None = None, last_seen_before: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try: return list_installations(session, page=page, page_size=page_size, client=client, health=health, version=version, search=search, last_seen_before=datetime.fromisoformat(last_seen_before) if last_seen_before else None)
finally: session.close()
def get_installation(session, agent_id: int):
row = session.execute(select(Installation, Agent, Tenant, AgentSnapshot).join(Agent, Agent.id == Installation.agent_id).join(Tenant, Tenant.id == Installation.tenant_id).outerjoin(AgentSnapshot, AgentSnapshot.installation_id == Installation.id).where(Agent.id == agent_id)).first()
if not row: raise HTTPException(404, "Agent installation not found")
return row
@routes.get("/agents/{agent_id}")
def agent_detail(agent_id: int, identity=Depends(identity)):
session, _, _ = identity
try:
installation, agent, tenant, snapshot = get_installation(session, agent_id)
return installation_view(installation, agent, tenant, snapshot)
finally: session.close()
@routes.get("/agents/{agent_id}/health")
def agent_health(agent_id: int, identity=Depends(identity)):
return agent_detail(agent_id, identity)
@routes.get("/agents/{agent_id}/coverage")
def agent_coverage(agent_id: int, identity=Depends(identity)):
detail = agent_detail(agent_id, identity)
return {key: detail[key] for key in ("total_products", "fiscally_complete", "safe_blocked", "failed_products", "coverage_percent", "blockers")}
@routes.get("/agents/{agent_id}/blockers")
def agent_blockers(agent_id: int, page: int = 1, page_size: int = 50, blocker_type: str | None = None, field: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try:
installation, agent, _, _ = get_installation(session, agent_id)
return list_blockers(session, tenant_id=installation.tenant_id, page=page, page_size=page_size, blocker_type=blocker_type, field=field)
finally: session.close()
@routes.get("/agents/{agent_id}/jobs")
def agent_jobs(agent_id: int, page: int = 1, page_size: int = 50, status: str | None = None, kind: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try: return list_jobs(session, agent_id=agent_id, page=page, page_size=page_size, status=status, kind=kind)
finally: session.close()
@routes.get("/agents/{agent_id}/events")
def agent_events(agent_id: int, page: int = 1, page_size: int = 50, severity: str | None = None, event_type: str | None = None, since: str | None = None, until: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try:
installation, _, _, _ = get_installation(session, agent_id)
return {"page": max(1, page), "page_size": min(100, max(1, page_size)), "items": list_panel_events(session, installation.id, page=page, page_size=page_size, severity=severity, event_type=event_type, since=datetime.fromisoformat(since) if since else None, until=datetime.fromisoformat(until) if until else None)}
finally: session.close()
@routes.get("/blockers/global")
def global_blockers(page: int = 1, page_size: int = 50, blocker_type: str | None = None, field: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try: return list_blockers(session, page=page, page_size=page_size, blocker_type=blocker_type, field=field)
finally: session.close()
@routes.get("/jobs/global")
def global_jobs(page: int = 1, page_size: int = 50, status: str | None = None, kind: str | None = None, since: str | None = None, until: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try: return list_jobs(session, page=page, page_size=page_size, status=status, kind=kind, since=datetime.fromisoformat(since) if since else None, until=datetime.fromisoformat(until) if until else None)
finally: session.close()
@routes.post("/agents/{agent_id}/actions/reprocess")
def reprocess(agent_id: int, payload: dict, request: Request, identity=Depends(identity)):
session, web_session, admin = identity
try:
require_csrf(web_session, payload.get("csrf_token"))
if payload.get("confirm") is not True: raise HTTPException(400, "explicit confirmation required")
installation, agent, _, _ = get_installation(session, agent_id)
kind = payload.get("kind", "REPROCESS_ALL")
if kind not in {"REPROCESS_ALL", "REPROCESS_PRODUCTS"}: raise HTTPException(400, "unsupported action")
job_payload = payload.get("payload") or {}
if kind == "REPROCESS_PRODUCTS" and not job_payload.get("codigos"): raise HTTPException(400, "codigos required")
job = create_job(session, agent, kind, job_payload)
record_audit(session, admin_user_id=admin.id, tenant_id=installation.tenant_id, action=kind, target_type="installation", target_id=installation.installation_id, context={"job_id": job.id, "request_ip": request.client.host if request.client else None})
session.commit()
return {"job_id": job.id, "status": job.status}
finally: session.close()
@routes.post("/blockers/{blocker_id}/resolve-human")
def resolve_blocker(blocker_id: int, payload: dict, request: Request, identity=Depends(identity)):
session, web_session, admin = identity
try:
require_csrf(web_session, payload.get("csrf_token"))
if payload.get("confirm") is not True or not str(payload.get("context", "")).strip(): raise HTTPException(400, "confirmation and explicit context required")
blocker = session.get(BlockedProduct, blocker_id)
if not blocker: raise HTTPException(404, "blocker not found")
blocker.resolved_at = datetime.now(timezone.utc); blocker.resolution_source = "HUMAN_PANEL"
record_audit(session, admin_user_id=admin.id, tenant_id=blocker.tenant_id, action="RESOLVE_BLOCKER", target_type="blocker", target_id=str(blocker_id), context={"context": payload["context"]})
session.commit()
return {"resolved": True, "blocker_id": blocker_id}
finally: session.close()
return routes