fix: validate control panel runtime paths
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -13,16 +13,17 @@ from sqlalchemy.orm import sessionmaker
|
||||
|
||||
def generate_fixture(session, agent_count=100, seed=7):
|
||||
now = datetime.now(timezone.utc)
|
||||
states = ["ONLINE"] * 55 + ["DEGRADED"] * 20 + ["OFFLINE"] * 15 + ["FAILED"] * 10
|
||||
states = ["ONLINE"] * 50 + ["DEGRADED"] * 20 + ["OFFLINE"] * 15 + ["FAILED"] * 10 + ["UNKNOWN"] * 5
|
||||
created = {"agents": 0, "events": 0, "blockers": 0, "jobs": 0}
|
||||
for index in range(agent_count):
|
||||
tenant = Tenant(name=f"Fixture Client {index:03d}"); session.add(tenant); session.flush()
|
||||
agent = Agent(tenant_id=tenant.id, name=f"fixture-agent-{index:03d}", token_hash=(f"fixture-{index:03d}".ljust(64, "0"))[:64], agent_version="1.0.0rc1"); session.add(agent); session.flush()
|
||||
agent_version = None if index % 17 == 0 else ("0.9.0" if index % 5 == 0 else "1.0.0rc1")
|
||||
agent = Agent(tenant_id=tenant.id, name=f"fixture-agent-{index:03d}", token_hash=(f"fixture-{index:03d}".ljust(64, "0"))[:64], agent_version=agent_version); session.add(agent); session.flush()
|
||||
state = states[index % len(states)]
|
||||
last_seen = now - timedelta(seconds=10 if state in {"ONLINE", "DEGRADED"} else 10_000)
|
||||
last_seen = None if state == "UNKNOWN" else now - timedelta(seconds=10 if state in {"ONLINE", "DEGRADED"} else 10_000)
|
||||
installation = Installation(agent_id=agent.id, tenant_id=tenant.id, installation_id=str(uuid.uuid5(uuid.NAMESPACE_DNS, f"fixture-{index}")), display_name=f"{tenant.name} - Servidor Principal", customer_code=f"F{index:03d}", hostname=f"srv-{index:03d}", company_name=f"Empresa {index:03d}", company_uf="BA", company_crt="1", last_seen_at=last_seen, last_status="IDLE")
|
||||
session.add(installation); session.flush()
|
||||
snapshot = AgentSnapshot(installation_id=installation.id, total_products=5000 + index, fiscally_complete=4900 + index, safe_blocked=index % 9, failed_products=1 if state == "FAILED" else 0, coverage_percent=98.0, blockers={"CFOP": index % 7, "ST": index % 5}, agent_health=state, last_job_status="FAILED" if state in {"DEGRADED", "FAILED"} else "COMPLETED")
|
||||
snapshot = AgentSnapshot(installation_id=installation.id, total_products=5000 + index, fiscally_complete=4900 + index, safe_blocked=index % 9, failed_products=1 if state == "FAILED" else 0, coverage_percent=98.0, blockers={"CFOP": index % 7, "ST": index % 5}, agent_health=state, version_drift="UNKNOWN" if agent_version is None else ("OUTDATED" if agent_version != "1.0.0rc1" else "CURRENT"), last_job_status="FAILED" if state in {"DEGRADED", "FAILED"} else "COMPLETED")
|
||||
session.add(snapshot); created["agents"] += 1
|
||||
for event_index in range(5):
|
||||
session.add(AgentEvent(tenant_id=tenant.id, installation_id=installation.id, agent_id=agent.id, severity="ERROR" if state == "FAILED" and event_index == 0 else "INFO", event_type="JOB_FAILED" if state in {"DEGRADED", "FAILED"} and event_index == 0 else "HEARTBEAT", message="Fixture operational event", structured_context={"fixture": True}, occurred_at=now - timedelta(minutes=event_index)))
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
<section class="cards compact"><article class="metric"><span>Produtos</span><strong>{{ detail.total_products }}</strong></article><article class="metric"><span>Completos</span><strong>{{ detail.fiscally_complete }}</strong></article><article class="metric"><span>Safe blocked</span><strong>{{ detail.safe_blocked }}</strong></article><article class="metric"><span>Coverage</span><strong>{{ "%.1f"|format(detail.coverage_percent) }}%</strong></article></section>
|
||||
<nav class="tabs">{% for name in ["overview","fiscal","blockers","jobs","events"] %}<a class="{% if tab==name %}active{% endif %}" href="?tab={{ name }}">{{ name }}</a>{% endfor %}</nav>
|
||||
<section class="split"><article class="panel"><div class="panel-title"><h2>Contexto da empresa</h2><form method="post" action="/agents/{{ detail.agent_id }}/reprocess" data-confirm="Disparar REPROCESS_ALL para esta instalação?"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="confirm" value="yes"><button class="secondary">Reprocessar tudo</button></form></div><dl class="facts"><dt>Empresa</dt><dd>{{ detail.company_name or "-" }}</dd><dt>UF / CRT</dt><dd>{{ detail.company_uf or "-" }} / {{ detail.company_crt or "-" }}</dd><dt>Firebird</dt><dd>{{ detail.firebird_version or "-" }} · ODS {{ detail.firebird_ods or "-" }}</dd><dt>Último heartbeat</dt><dd>{{ detail.last_seen_at or "-" }}</dd><dt>Último job</dt><dd>{{ detail.last_job_status or "-" }} · {{ detail.last_job_at or "-" }}</dd></dl></article><article class="panel"><h2>Eventos recentes</h2>{% for event in events[:8] %}<div class="event-row"><span class="event-time">{{ event.occurred_at }}</span><span><b>{{ event.event_type }}</b><small>{{ event.message }}</small></span><em class="severity {{ event.severity|lower }}">{{ event.severity }}</em></div>{% else %}<p class="muted">Nenhum evento.</p>{% endfor %}</article></section>
|
||||
<section class="panel"><div class="panel-title"><h2>Blockers abertos</h2><a href="/blockers">Ver todos</a></div>{% for item in blockers.items[:8] %}<div class="list-row"><span><b>{{ item.product_code }}</b> · {{ item.field }}<small>{{ item.reason }}</small></span><em class="badge blocked">{{ item.blocker_type }}</em></div>{% else %}<p class="muted">Nenhum blocker aberto.</p>{% endfor %}</section>
|
||||
<section class="panel"><div class="panel-title"><h2>Blockers abertos</h2><a href="/blockers">Ver todos</a></div>{% for item in blockers["items"][:8] %}<div class="list-row"><span><b>{{ item.product_code }}</b> · {{ item.field }}<small>{{ item.reason }}</small></span><em class="badge blocked">{{ item.blocker_type }}</em></div>{% else %}<p class="muted">Nenhum blocker aberto.</p>{% endfor %}</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{% extends "base.html" %}{% block title %}Agents | GTI Fiscal{% endblock %}{% block content %}<div class="page-heading"><div><p class="eyebrow">INVENTÁRIO OPERACIONAL</p><h1>Agents / Servidores</h1></div><form class="filters"><input name="search" value="{{ search }}" placeholder="Cliente, instalação ou hostname"><select name="health"><option value="">Todos os health</option>{% for value in ["ONLINE","DEGRADED","OFFLINE","FAILED","UNKNOWN"] %}<option {% if health_filter==value %}selected{% endif %}>{{ value }}</option>{% endfor %}</select><button class="secondary">Filtrar</button></form></div><div class="panel table-wrap"><table><thead><tr><th>Cliente / instalação</th><th>Servidor</th><th>Empresa</th><th>Health</th><th>Versão</th><th>Produtos</th><th>Complete</th><th>Blocked</th><th>Last seen</th></tr></thead><tbody>{% for item in result.items %}<tr><td><a href="/agents/{{ item.agent_id }}"><b>{{ item.client }}</b><small>{{ item.display_name }}</small></a></td><td>{{ item.hostname or "-" }}</td><td>{{ item.company_name or item.company_code or "-" }}<small>{{ item.company_uf or "" }} {{ item.company_crt or "" }}</small></td><td><span class="badge {{ item.health|lower }}">{{ item.health }}</span></td><td>{{ item.agent_version or "UNKNOWN" }}<small>{{ item.version_drift }}</small></td><td>{{ item.total_products }}</td><td>{{ item.fiscally_complete }}</td><td>{{ item.safe_blocked }}</td><td>{{ item.last_seen_at or "Never" }}</td></tr>{% else %}<tr><td colspan="9" class="muted">Nenhuma instalação encontrada.</td></tr>{% endfor %}</tbody></table></div>{% endblock %}
|
||||
{% extends "base.html" %}{% block title %}Agents | GTI Fiscal{% endblock %}{% block content %}<div class="page-heading"><div><p class="eyebrow">INVENTÁRIO OPERACIONAL</p><h1>Agents / Servidores</h1></div><form class="filters"><input name="search" value="{{ search }}" placeholder="Cliente, instalação ou hostname"><select name="health"><option value="">Todos os health</option>{% for value in ["ONLINE","DEGRADED","OFFLINE","FAILED","UNKNOWN"] %}<option {% if health_filter==value %}selected{% endif %}>{{ value }}</option>{% endfor %}</select><button class="secondary">Filtrar</button></form></div><div class="panel table-wrap"><table><thead><tr><th>Cliente / instalação</th><th>Servidor</th><th>Empresa</th><th>Health</th><th>Versão</th><th>Produtos</th><th>Complete</th><th>Blocked</th><th>Last seen</th></tr></thead><tbody>{% for item in result["items"] %}<tr><td><a href="/agents/{{ item.agent_id }}"><b>{{ item.client }}</b><small>{{ item.display_name }}</small></a></td><td>{{ item.hostname or "-" }}</td><td>{{ item.company_name or item.company_code or "-" }}<small>{{ item.company_uf or "" }} {{ item.company_crt or "" }}</small></td><td><span class="badge {{ item.health|lower }}">{{ item.health }}</span></td><td>{{ item.agent_version or "UNKNOWN" }}<small>{{ item.version_drift }}</small></td><td>{{ item.total_products }}</td><td>{{ item.fiscally_complete }}</td><td>{{ item.safe_blocked }}</td><td>{{ item.last_seen_at or "Never" }}</td></tr>{% else %}<tr><td colspan="9" class="muted">Nenhuma instalação encontrada.</td></tr>{% endfor %}</tbody></table></div>{% endblock %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="pt-BR">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{% block title %}GTI Fiscal Control Panel{% endblock %}</title><link rel="stylesheet" href="/static/control-panel.css"></head>
|
||||
<body>
|
||||
<header class="topbar"><a class="brand" href="/">GTI <span>FISCAL</span></a><nav><a href="/">Resumo</a><a href="/agents">Agents</a><a href="/blockers">Blockers</a><a href="/jobs">Jobs</a></nav>{% if auth %}<form method="post" action="/logout"><button class="ghost">Sair</button></form>{% endif %}</header>
|
||||
<header class="topbar"><a class="brand" href="/">GTI <span>FISCAL</span></a><nav><a href="/">Resumo</a><a href="/agents">Agents</a><a href="/blockers">Blockers</a><a href="/jobs">Jobs</a></nav>{% if auth %}<form method="post" action="/logout"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Sair</button></form>{% endif %}</header>
|
||||
<main class="shell">{% block content %}{% endblock %}</main>
|
||||
<script src="/static/control-panel.js"></script>
|
||||
</body></html>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{% extends "base.html" %}{% block title %}Blockers | GTI Fiscal{% endblock %}{% block content %}<div class="page-heading"><div><p class="eyebrow">FILA FISCAL</p><h1>Blockers globais</h1></div></div><div class="panel table-wrap"><table><thead><tr><th>Cliente</th><th>Produto</th><th>Campo</th><th>Tipo</th><th>Motivo</th><th>Última ocorrência</th><th>Ação</th></tr></thead><tbody>{% for item in result.items %}<tr><td>{{ item.client or item.tenant_id }}</td><td><b>{{ item.product_code }}</b><small>{{ item.description or "" }}</small></td><td>{{ item.field }}</td><td><span class="badge blocked">{{ item.blocker_type }}</span></td><td>{{ item.reason }}</td><td>{{ item.last_seen_at }}<small>{{ item.retry_count }} retries</small></td><td><form method="post" action="/blockers/{{ item.id }}/resolve" data-confirm="Resolver este blocker com contexto explícito?"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="confirm" value="yes"><input type="hidden" name="context" value="Resolved after operator review"><button class="secondary">Resolver</button></form></td></tr>{% else %}<tr><td colspan="7" class="muted">Nenhum blocker aberto.</td></tr>{% endfor %}</tbody></table></div>{% endblock %}
|
||||
{% extends "base.html" %}{% block title %}Blockers | GTI Fiscal{% endblock %}{% block content %}<div class="page-heading"><div><p class="eyebrow">FILA FISCAL</p><h1>Blockers globais</h1></div></div><div class="panel table-wrap"><table><thead><tr><th>Cliente</th><th>Produto</th><th>Campo</th><th>Tipo</th><th>Motivo</th><th>Última ocorrência</th><th>Ação</th></tr></thead><tbody>{% for item in result["items"] %}<tr><td>{{ item.client or item.tenant_id }}</td><td><b>{{ item.product_code }}</b><small>{{ item.description or "" }}</small></td><td>{{ item.field }}</td><td><span class="badge blocked">{{ item.blocker_type }}</span></td><td>{{ item.reason }}</td><td>{{ item.last_seen_at }}<small>{{ item.retry_count }} retries</small></td><td><form method="post" action="/blockers/{{ item.id }}/resolve" data-confirm="Resolver este blocker com contexto explícito?"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="confirm" value="yes"><input type="hidden" name="context" value="Resolved after operator review"><button class="secondary">Resolver</button></form></td></tr>{% else %}<tr><td colspan="7" class="muted">Nenhum blocker aberto.</td></tr>{% endfor %}</tbody></table></div>{% endblock %}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{% extends "base.html" %}{% block title %}Jobs | GTI Fiscal{% endblock %}{% block content %}<div class="page-heading"><div><p class="eyebrow">EXECUÇÕES</p><h1>Jobs globais</h1></div></div><div class="panel table-wrap"><table><thead><tr><th>Job</th><th>Agent</th><th>Kind</th><th>Status</th><th>Created</th><th>Finished</th><th>Error</th></tr></thead><tbody>{% for item in result.items %}<tr><td>#{{ item.job_id }}</td><td>{{ item.agent_id }}</td><td>{{ item.kind }}</td><td><span class="badge {{ item.status|lower }}">{{ item.status }}</span></td><td>{{ item.created_at }}</td><td>{{ item.finished_at or "-" }}</td><td class="error-text">{{ item.error or "-" }}</td></tr>{% else %}<tr><td colspan="7" class="muted">Nenhum job encontrado.</td></tr>{% endfor %}</tbody></table></div>{% endblock %}
|
||||
{% extends "base.html" %}{% block title %}Jobs | GTI Fiscal{% endblock %}{% block content %}<div class="page-heading"><div><p class="eyebrow">EXECUÇÕES</p><h1>Jobs globais</h1></div></div><div class="panel table-wrap"><table><thead><tr><th>Job</th><th>Agent</th><th>Kind</th><th>Status</th><th>Created</th><th>Finished</th><th>Error</th></tr></thead><tbody>{% for item in result["items"] %}<tr><td>#{{ item.job_id }}</td><td>{{ item.agent_id }}</td><td>{{ item.kind }}</td><td><span class="badge {{ item.status|lower }}">{{ item.status }}</span></td><td>{{ item.created_at }}</td><td>{{ item.finished_at or "-" }}</td><td class="error-text">{{ item.error or "-" }}</td></tr>{% else %}<tr><td colspan="7" class="muted">Nenhum job encontrado.</td></tr>{% endfor %}</tbody></table></div>{% endblock %}
|
||||
|
||||
@@ -11,4 +11,5 @@ def test_fixture_creates_one_hundred_varied_installations_and_events():
|
||||
assert session.query(Installation).count() == 100
|
||||
assert session.query(AgentEvent).count() == 500
|
||||
assert session.query(Job).count() == 100
|
||||
assert {row.agent_health for row in session.query(AgentSnapshot).all()} == {"ONLINE", "DEGRADED", "OFFLINE", "FAILED"}
|
||||
assert {row.agent_health for row in session.query(AgentSnapshot).all()} == {"ONLINE", "DEGRADED", "OFFLINE", "FAILED", "UNKNOWN"}
|
||||
assert {row.version_drift for row in session.query(AgentSnapshot).all()} == {"CURRENT", "OUTDATED", "UNKNOWN"}
|
||||
|
||||
@@ -64,6 +64,7 @@ def test_human_login_and_dashboard_are_separate_from_agent_bearer():
|
||||
response = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
|
||||
assert response.status_code == 303
|
||||
assert client.get("/v1/dashboard/summary").status_code == 200
|
||||
assert client.get("/v1/jobs/global").status_code == 200
|
||||
assert TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None)).get("/v1/dashboard/summary", headers={"Authorization": "Bearer x"}).status_code == 401
|
||||
assert client.get("/v1/agents").json()["items"][0]["installation_id"]
|
||||
csrf = client.cookies.get("gti_csrf")
|
||||
@@ -71,3 +72,5 @@ def test_human_login_and_dashboard_are_separate_from_agent_bearer():
|
||||
assert action.status_code == 200 and action.json()["status"] == "PENDING"
|
||||
denied = client.post("/v1/agents/1/actions/reprocess", json={"confirm": True, "csrf_token": "wrong", "kind": "REPROCESS_ALL"})
|
||||
assert denied.status_code == 403
|
||||
assert client.post("/logout").status_code == 403
|
||||
assert client.post("/logout", data={"csrf_token": csrf}, follow_redirects=False).status_code == 303
|
||||
|
||||
Reference in New Issue
Block a user