sync: 193 file da Baida98/AI@8845b81e (2026-08-29 21:08 UTC)

#147
by Baida07 - opened
api/public_snapshot.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Writer idempotente per lo snapshot pubblico operativo.
2
+
3
+ Lo snapshot è un DTO sanificato e singleton: viene aggiornato dal backend con
4
+ service_role e letto dall'endpoint pubblico senza esporre task, sessioni o log.
5
+ Un errore di persistenza non deve impedire l'avvio del servizio.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ from typing import Any
12
+
13
+ from .state import _agent_tasks, _loop_registry, sb
14
+ from .version import RUNTIME_VERSION
15
+
16
+ _logger = logging.getLogger("agente_ai.public_snapshot")
17
+
18
+ _ACTIVE_STATUSES = {"RUNNING", "IN_PROGRESS", "EXECUTING", "PROCESSING"}
19
+
20
+
21
+ def _snapshot_row() -> dict[str, Any]:
22
+ tasks = list(_agent_tasks.values())
23
+ return {
24
+ "singleton": True,
25
+ "service_status": "operational",
26
+ "active_sessions": len(_loop_registry),
27
+ "queued_tasks": sum(1 for task in tasks if str(task.get("status", "")).upper() == "QUEUED"),
28
+ "in_progress_tasks": sum(1 for task in tasks if str(task.get("status", "")).upper() in _ACTIVE_STATUSES),
29
+ "app_version": RUNTIME_VERSION,
30
+ }
31
+
32
+
33
+ async def write_public_dashboard_snapshot() -> bool:
34
+ """Upsert the singleton public snapshot without blocking application startup.
35
+
36
+ Returns ``False`` on missing Supabase or a schema/permission error. The
37
+ public status route remains available with its degraded response while the
38
+ service continues booting; the exact exception is kept in backend logs.
39
+ """
40
+ client = sb()
41
+ if client is None:
42
+ _logger.warning("BOOT: public snapshot skipped — Supabase client unavailable")
43
+ return False
44
+
45
+ row = _snapshot_row()
46
+
47
+ def operation() -> None:
48
+ client.table("public_dashboard_snapshot").upsert(row, on_conflict="singleton").execute()
49
+
50
+ last_error: Exception | None = None
51
+ for attempt in range(1, 4):
52
+ try:
53
+ await asyncio.to_thread(operation)
54
+ _logger.info(
55
+ "BOOT: public snapshot upserted status=%s sessions=%d queued=%d in_progress=%d version=%s",
56
+ row["service_status"], row["active_sessions"], row["queued_tasks"],
57
+ row["in_progress_tasks"], row["app_version"],
58
+ )
59
+ return True
60
+ except Exception as exc:
61
+ last_error = exc
62
+ if attempt < 3:
63
+ await asyncio.sleep(2)
64
+
65
+ _logger.warning(
66
+ "BOOT: public snapshot upsert failed after retries (%s); public status remains degraded",
67
+ type(last_error).__name__ if last_error else "unknown",
68
+ )
69
+ return False
70
+
71
+
72
+ async def refresh_public_dashboard_snapshot() -> bool:
73
+ """Alias for future heartbeat refreshes; intentionally idempotent."""
74
+ return await write_public_dashboard_snapshot()
main.py CHANGED
@@ -88,7 +88,24 @@ async def _run_auto_migration():
88
  TO service_role
89
  USING (true)
90
  WITH CHECK (true);
91
- -- 5. Healthcheck function
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  CREATE OR REPLACE FUNCTION public.health_check()
93
  RETURNS jsonb AS $$
94
  BEGIN
@@ -236,6 +253,12 @@ async def startup_event():
236
  except Exception as e:
237
  _logger.warning(f"⚠️ BOOT: apply_rls_fix_sync() fallito (non bloccante): {e}")
238
  asyncio.create_task(_run_auto_migration())
 
 
 
 
 
 
239
  try:
240
  from api.providers import start_heartbeat
241
  start_heartbeat()
 
88
  TO service_role
89
  USING (true)
90
  WITH CHECK (true);
91
+ -- 5. Public dashboard snapshot used by /api/public/status
92
+ CREATE TABLE IF NOT EXISTS public.public_dashboard_snapshot (
93
+ singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
94
+ service_status text NOT NULL DEFAULT 'operational',
95
+ active_sessions integer NOT NULL DEFAULT 0,
96
+ queued_tasks integer NOT NULL DEFAULT 0,
97
+ in_progress_tasks integer NOT NULL DEFAULT 0,
98
+ app_version text,
99
+ updated_at timestamptz NOT NULL DEFAULT now()
100
+ );
101
+ ALTER TABLE public.public_dashboard_snapshot ENABLE ROW LEVEL SECURITY;
102
+ GRANT SELECT ON public.public_dashboard_snapshot TO anon, authenticated;
103
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.public_dashboard_snapshot TO service_role;
104
+ DROP POLICY IF EXISTS "public_dashboard_snapshot_read" ON public.public_dashboard_snapshot;
105
+ CREATE POLICY "public_dashboard_snapshot_read" ON public.public_dashboard_snapshot
106
+ FOR SELECT TO anon, authenticated USING (true);
107
+
108
+ -- 6. Healthcheck function
109
  CREATE OR REPLACE FUNCTION public.health_check()
110
  RETURNS jsonb AS $$
111
  BEGIN
 
253
  except Exception as e:
254
  _logger.warning(f"⚠️ BOOT: apply_rls_fix_sync() fallito (non bloccante): {e}")
255
  asyncio.create_task(_run_auto_migration())
256
+ try:
257
+ from api.public_snapshot import write_public_dashboard_snapshot
258
+ asyncio.create_task(write_public_dashboard_snapshot())
259
+ _logger.info("✅ BOOT: public dashboard snapshot writer avviato.")
260
+ except Exception as e:
261
+ _logger.warning(f"⚠️ BOOT: avvio public snapshot writer fallito (non bloccante): {e}")
262
  try:
263
  from api.providers import start_heartbeat
264
  start_heartbeat()
tests/test_public_snapshot_writer.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+
3
+ from api import public_snapshot
4
+
5
+
6
+ class _FakeTable:
7
+ def __init__(self, calls):
8
+ self.calls = calls
9
+
10
+ def upsert(self, row, on_conflict=None):
11
+ self.calls.append((row, on_conflict))
12
+ return self
13
+
14
+ def execute(self):
15
+ return object()
16
+
17
+
18
+ class _FakeClient:
19
+ def __init__(self):
20
+ self.calls = []
21
+
22
+ def table(self, name):
23
+ assert name == "public_dashboard_snapshot"
24
+ return _FakeTable(self.calls)
25
+
26
+
27
+ def test_snapshot_writer_is_idempotent_and_counts_runtime_state(monkeypatch):
28
+ fake = _FakeClient()
29
+ monkeypatch.setattr(public_snapshot, "sb", lambda: fake)
30
+ monkeypatch.setattr(
31
+ public_snapshot,
32
+ "_agent_tasks",
33
+ {
34
+ "queued": {"status": "QUEUED"},
35
+ "running": {"status": "RUNNING"},
36
+ "done": {"status": "SUCCESS"},
37
+ },
38
+ )
39
+ monkeypatch.setattr(public_snapshot, "_loop_registry", {"session-1": object()})
40
+
41
+ assert asyncio.run(public_snapshot.write_public_dashboard_snapshot()) is True
42
+ assert asyncio.run(public_snapshot.write_public_dashboard_snapshot()) is True
43
+
44
+ assert len(fake.calls) == 2
45
+ first, second = fake.calls
46
+ assert first[1] == "singleton"
47
+ assert second[1] == "singleton"
48
+ assert first[0] == second[0]
49
+ assert first[0]["singleton"] is True
50
+ assert first[0]["active_sessions"] == 1
51
+ assert first[0]["queued_tasks"] == 1
52
+ assert first[0]["in_progress_tasks"] == 1
53
+ assert first[0]["service_status"] == "operational"
54
+
55
+
56
+ def test_snapshot_writer_is_non_blocking_when_supabase_is_unavailable(monkeypatch):
57
+ monkeypatch.setattr(public_snapshot, "sb", lambda: None)
58
+ assert asyncio.run(public_snapshot.write_public_dashboard_snapshot()) is False