fix: validate control panel runtime paths

This commit is contained in:
gti
2026-09-22 16:23:51 -03:00
parent 66b5888d80
commit 6bdac245b9
12 changed files with 42 additions and 20 deletions
+11
View File
@@ -16,6 +16,8 @@ from .context_keys import validate_entry
from .models import AgentSnapshot, BlockedProduct, FiscalContextEntry, ClassificationCache, ClassificationRequest, ClassificationResult, Dataset, Installation, Job
from .events import record_event
from .installation_identity import compute_agent_health, compute_version_drift, sanitize_registration
from .control_plane import list_jobs
from .human_auth import require_human
logger = logging.getLogger("gti_fiscal_hub")
@@ -331,6 +333,15 @@ def router(session_factory, bootstrap_token):
return _job_view(job)
finally: session.close()
@routes.get("/jobs/global")
def global_jobs(request: Request, page: int = 1, page_size: int = 50, status: str | None = None, kind: str | None = None):
session = session_factory()
try:
require_human(request, session)
return list_jobs(session, page=page, page_size=page_size, status=status, kind=kind)
finally:
session.close()
@routes.get("/jobs/{job_id}")
def get_job(job_id: int, identity=Depends(principal)):
session, agent = identity
+5 -3
View File
@@ -8,7 +8,7 @@ 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 .models import Agent, AgentSnapshot, BlockedProduct, Installation, Job, Tenant
from .jobs import create_job
@@ -102,7 +102,8 @@ def router(session_factory):
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)
existing = next((candidate for candidate in session.scalars(select(Job).where(Job.agent_id == agent.id, Job.kind == kind, Job.status == "PENDING")).all() if candidate.payload == job_payload), None)
job = existing or 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}
@@ -116,7 +117,8 @@ def router(session_factory):
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"
if blocker.resolved_at is None:
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}
+3 -3
View File
@@ -83,12 +83,12 @@ def compute_agent_health(snapshot, now: datetime | None = None, *, supplemental=
return "UNKNOWN", "no heartbeat received"
if last_seen.tzinfo is None:
last_seen = last_seen.replace(tzinfo=timezone.utc)
offline_after = settings.agent_offline_after_seconds or settings.agent_heartbeat_interval_seconds * 3
if (now - last_seen).total_seconds() > offline_after:
return "OFFLINE", f"last heartbeat exceeds {offline_after}s"
source = supplemental or snapshot
if any(getattr(candidate, "agent_health", None) == "FAILED" or getattr(candidate, "last_status", None) == "FAILED" for candidate in (snapshot, supplemental) if candidate is not None):
return "FAILED", "persistent technical failure"
offline_after = settings.agent_offline_after_seconds or settings.agent_heartbeat_interval_seconds * 3
if (now - last_seen).total_seconds() > offline_after:
return "OFFLINE", f"last heartbeat exceeds {offline_after}s"
if getattr(source, "failed_products", 0) or getattr(source, "last_job_status", "") == "FAILED":
return "DEGRADED", "recent technical or job failure"
return "ONLINE", "recent heartbeat without critical technical failure"
+8 -4
View File
@@ -10,7 +10,7 @@ from sqlalchemy import select
from .config import settings
from .control_plane import dashboard_summary, list_blockers, list_installations, list_jobs, list_panel_events
from .human_auth import create_session, current_session, require_csrf, verify_password
from .models import AdminUser, Installation, Agent
from .models import AdminUser, Installation, Agent, Job
from .jobs import create_job
from .audit import record_audit
@@ -23,7 +23,8 @@ def router(session_factory):
return current_session(request, session)
def render(request, template, context=None, status_code=200):
return templates.TemplateResponse(template, {"request": request, **(context or {})}, status_code=status_code)
values = {"request": request, "csrf_token": request.cookies.get("gti_csrf", ""), **(context or {})}
return templates.TemplateResponse(request=request, name=template, context=values, status_code=status_code)
@routes.get("/login", response_class=HTMLResponse)
def login_page(request: Request):
@@ -43,10 +44,12 @@ def router(session_factory):
return response
@routes.post("/logout")
def logout(request: Request):
async def logout(request: Request):
with session_factory() as session:
web_session, admin = current_session(request, session)
if web_session:
values = await form_values(request)
require_csrf(web_session, values.get("csrf_token"))
session.delete(web_session); session.commit()
response = RedirectResponse("/login", status_code=303)
response.delete_cookie(settings.web_cookie_name); response.delete_cookie("gti_csrf")
@@ -99,7 +102,8 @@ def router(session_factory):
agent = session.get(Agent, agent_id)
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
if not agent or not installation: return HTMLResponse("Agent not found", status_code=404)
job = create_job(session, agent, "REPROCESS_ALL", {})
existing = next((candidate for candidate in session.scalars(select(Job).where(Job.agent_id == agent.id, Job.kind == "REPROCESS_ALL", Job.status == "PENDING")).all() if candidate.payload == {}), None)
job = existing or create_job(session, agent, "REPROCESS_ALL", {})
record_audit(session, admin_user_id=admin.id, tenant_id=installation.tenant_id, action="REPROCESS_ALL", target_type="installation", target_id=installation.installation_id, context={"job_id": job.id})
session.commit()
return RedirectResponse(f"/agents/{agent_id}?tab=jobs", status_code=303)