From 2597ca9110094c447b8672e0cfe93ebb7fd02ab1 Mon Sep 17 00:00:00 2001 From: Dom Date: Sat, 27 Jun 2026 10:47:08 +0200 Subject: [PATCH] feat(server): endpoint GET /api/v1/agents/logs/{machine_id} (push-log-DGX, brique 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route de diagnostic dashboard (read-only) : restitue les logs poussés par un poste, rangés par machine_id. Bearer global ; volontairement sans garde fleet (consultation d'un poste révoqué/en panne). limit=tail pour borner la réponse. 4 tests d'intégration verts ; store inchangé (briques 1-2 figées). Co-Authored-By: Claude Opus 4.8 (1M context) --- agent_v0/server_v1/api_stream.py | 24 +++++++++ tests/integration/test_agent_logs_api.py | 69 ++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/agent_v0/server_v1/api_stream.py b/agent_v0/server_v1/api_stream.py index 1723b8f07..e7b40675d 100644 --- a/agent_v0/server_v1/api_stream.py +++ b/agent_v0/server_v1/api_stream.py @@ -7250,6 +7250,30 @@ async def agents_logs(request: AgentLogsRequest): return {"status": "ok", "received": received, "machine_id": machine_id} +@app.get("/api/v1/agents/logs/{machine_id}") +async def get_agents_logs(machine_id: str, limit: int = 1000): + """Lecture des logs poussés par un poste (push-log-DGX, brique 3). + + Route de diagnostic dashboard : restitue les logs rangés par machine_id + (poste sans AnyDesk). Lecture admin read-only — volontairement SANS garde + fleet : on doit pouvoir consulter un poste révoqué ou en panne. Seul le + Bearer (dépendance globale `_verify_token`) protège l'accès. + + `limit` borne la réponse aux N entrées les plus récentes (tail) pour éviter + de renvoyer plusieurs jours de logs d'un coup. + """ + entries = agent_logs_store.read(machine_id) + total = len(entries) + if limit and limit > 0: + entries = entries[-limit:] + return { + "machine_id": machine_id, + "total": total, + "count": len(entries), + "logs": entries, + } + + # ========================================================================= # R2 MVP P0 — DialogResolver (catalogue centralisé des modaux runtime) # Flag OFF par défaut. Activer en posant RPA_DIALOG_RESOLVER_ENABLED=true. diff --git a/tests/integration/test_agent_logs_api.py b/tests/integration/test_agent_logs_api.py index b03fefe4f..5a02af552 100644 --- a/tests/integration/test_agent_logs_api.py +++ b/tests/integration/test_agent_logs_api.py @@ -121,3 +121,72 @@ def test_post_logs_rejects_oversized_batch(logs_client): assert resp.status_code == 413, resp.text assert store.read("lea-flood") == [] + + +# --------------------------------------------------------------------------- +# Brique 3 — lecture des logs par machine_id (route dashboard, read-only). +# Lecture admin/diagnostic : PAS de garde fleet (on veut justement pouvoir +# consulter un poste révoqué ou en panne) ; seul le Bearer protège. +# --------------------------------------------------------------------------- + + +def test_get_logs_returns_persisted_for_machine(logs_client): + """GET /agents/logs/{machine_id} restitue les logs stockés, dans l'ordre.""" + client, token, store = logs_client + store.append( + "lea-emilie-001", + [ + {"ts": "2026-06-26T16:00:00", "level": "INFO", "message": "demarrage"}, + {"ts": "2026-06-26T16:00:01", "level": "WARNING", "message": "popup"}, + ], + ) + + resp = client.get( + "/api/v1/agents/logs/lea-emilie-001", headers=_auth_headers(token) + ) + + assert resp.status_code == 200, resp.text + body = resp.json() + assert body["machine_id"] == "lea-emilie-001" + assert body["count"] == 2 + assert body["total"] == 2 + assert body["logs"][0]["message"] == "demarrage" + assert body["logs"][1]["level"] == "WARNING" + + +def test_get_logs_without_token_returns_401(logs_client): + client, _, _ = logs_client + resp = client.get("/api/v1/agents/logs/lea-emilie-001") + assert resp.status_code == 401 + + +def test_get_logs_empty_for_unknown_machine(logs_client): + """Un poste sans log remonte une liste vide (200), pas une erreur.""" + client, token, _ = logs_client + resp = client.get( + "/api/v1/agents/logs/lea-inconnu", headers=_auth_headers(token) + ) + assert resp.status_code == 200, resp.text + body = resp.json() + assert body["count"] == 0 + assert body["total"] == 0 + assert body["logs"] == [] + + +def test_get_logs_limit_returns_tail(logs_client): + """`limit` borne la réponse aux N entrées les plus récentes (tail).""" + client, token, store = logs_client + store.append( + "lea-tail", + [{"level": "INFO", "message": f"m{i}"} for i in range(5)], + ) + + resp = client.get( + "/api/v1/agents/logs/lea-tail?limit=2", headers=_auth_headers(token) + ) + + assert resp.status_code == 200, resp.text + body = resp.json() + assert body["total"] == 5 + assert body["count"] == 2 + assert [e["message"] for e in body["logs"]] == ["m3", "m4"]