Baida07 commited on
Commit
cd0cc7d
Β·
verified Β·
1 Parent(s): 36c824b

sync: 172 file da Baida98/AI@841e91e3 (2026-08-23 10:54 UTC) [deploy-all]

Browse files
Files changed (2) hide show
  1. api/public_status.py +23 -4
  2. api/terminal.py +49 -0
api/public_status.py CHANGED
@@ -10,7 +10,7 @@ import asyncio
10
  import logging
11
  from typing import Any
12
 
13
- from fastapi import APIRouter, HTTPException
14
 
15
  from .state import sb
16
 
@@ -28,7 +28,7 @@ async def public_status() -> dict[str, Any]:
28
  """Restituisce esclusivamente lo snapshot deliberatamente pubblico."""
29
  client = sb()
30
  if client is None:
31
- raise HTTPException(status_code=503, detail="Public status non configurato")
32
 
33
  def operation():
34
  return client.table("public_dashboard_snapshot").select(_PUBLIC_FIELDS).eq("singleton", True).limit(1).execute()
@@ -37,11 +37,11 @@ async def public_status() -> dict[str, Any]:
37
  result = await asyncio.to_thread(operation)
38
  except Exception as exc:
39
  _logger.warning("public status snapshot unavailable: %s", type(exc).__name__)
40
- raise HTTPException(status_code=503, detail="Public status temporaneamente non disponibile") from exc
41
 
42
  row = (result.data or [None])[0]
43
  if not row:
44
- raise HTTPException(status_code=503, detail="Public status snapshot non inizializzato")
45
 
46
  return {
47
  "service_status": str(row.get("service_status") or "unknown"),
@@ -51,3 +51,22 @@ async def public_status() -> dict[str, Any]:
51
  "app_version": row.get("app_version"),
52
  "updated_at": row.get("updated_at"),
53
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  import logging
11
  from typing import Any
12
 
13
+ from fastapi import APIRouter
14
 
15
  from .state import sb
16
 
 
28
  """Restituisce esclusivamente lo snapshot deliberatamente pubblico."""
29
  client = sb()
30
  if client is None:
31
+ return _degraded_snapshot("database_unavailable")
32
 
33
  def operation():
34
  return client.table("public_dashboard_snapshot").select(_PUBLIC_FIELDS).eq("singleton", True).limit(1).execute()
 
37
  result = await asyncio.to_thread(operation)
38
  except Exception as exc:
39
  _logger.warning("public status snapshot unavailable: %s", type(exc).__name__)
40
+ return _degraded_snapshot("snapshot_unavailable")
41
 
42
  row = (result.data or [None])[0]
43
  if not row:
44
+ return _degraded_snapshot("snapshot_not_initialized")
45
 
46
  return {
47
  "service_status": str(row.get("service_status") or "unknown"),
 
51
  "app_version": row.get("app_version"),
52
  "updated_at": row.get("updated_at"),
53
  }
54
+
55
+
56
+ def _degraded_snapshot(reason: str) -> dict[str, Any]:
57
+ """Safe public response while the operational snapshot is unavailable.
58
+
59
+ The public endpoint is used by lightweight status surfaces. Returning a
60
+ deliberate degraded state keeps those surfaces functional without
61
+ exposing database errors, internal topology, or operational records.
62
+ """
63
+ return {
64
+ "service_status": "degraded",
65
+ "active_sessions": 0,
66
+ "queued_tasks": 0,
67
+ "in_progress_tasks": 0,
68
+ "app_version": None,
69
+ "updated_at": None,
70
+ "degraded": True,
71
+ "reason": reason,
72
+ }
api/terminal.py CHANGED
@@ -1,5 +1,6 @@
1
  """backend/api/terminal.py β€” WebSocket PTY terminal (S354 + S754-B + S755)."""
2
  import os, asyncio, pty, fcntl, struct, termios, json, shlex, time, logging
 
3
  from pathlib import Path
4
  from fastapi import APIRouter, WebSocket, WebSocketDisconnect
5
  from fastapi import Depends
@@ -8,6 +9,31 @@ from .auth_guard import require_role, AuthRole
8
  router = APIRouter()
9
  _logger = logging.getLogger("terminal")
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  # ── Startup script (S755) ─────────────────────────────────────────────────────
12
  # Scritto in /data/.bashrc_agente e sourciate da bash via --rcfile.
13
  # Configura venv Python + npm persistenti, Playwright, workspace, aliases, prompt.
@@ -239,6 +265,25 @@ async def terminal_packages(role: AuthRole = Depends(require_role(AuthRole.MACHI
239
  'generated_at': int(time.time()),
240
  }
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  @router.websocket('/ws/terminal')
243
  async def terminal_ws(ws: WebSocket):
244
  """
@@ -263,6 +308,8 @@ async def terminal_ws(ws: WebSocket):
263
  await ws.close(code=4403)
264
  return
265
  await ws.accept()
 
 
266
  loop = asyncio.get_event_loop()
267
 
268
  # S755: assicura che /data/.bashrc_agente esista e sia aggiornato
@@ -327,6 +374,7 @@ async def terminal_ws(ws: WebSocket):
327
  try:
328
  data = await loop.run_in_executor(None, lambda: os.read(master_fd, 4096))
329
  if data:
 
330
  await ws.send_bytes(data)
331
  # S754-B: salvataggio periodico ogni 60s durante attivitΓ 
332
  _now = time.monotonic()
@@ -361,6 +409,7 @@ async def terminal_ws(ws: WebSocket):
361
  try:
362
  await asyncio.gather(_reader(), _writer())
363
  finally:
 
364
  closed.set()
365
  # S754-B: salva lo stato prima di terminare il processo.
366
  # La sessione tmux Γ¨ ancora viva qui (proc Γ¨ il CLIENT tmux, non il SERVER).
 
1
  """backend/api/terminal.py β€” WebSocket PTY terminal (S354 + S754-B + S755)."""
2
  import os, asyncio, pty, fcntl, struct, termios, json, shlex, time, logging
3
+ from collections import defaultdict, deque
4
  from pathlib import Path
5
  from fastapi import APIRouter, WebSocket, WebSocketDisconnect
6
  from fastapi import Depends
 
9
  router = APIRouter()
10
  _logger = logging.getLogger("terminal")
11
 
12
+ # Recent PTY output used by the authenticated auto-repair diagnostic.
13
+ # Buffers are intentionally process-local and bounded: they are diagnostics,
14
+ # not a second persistence channel for terminal sessions.
15
+ _BUFFER_MAX_CHUNKS = 200
16
+ _BUFFER_MAX_CHARS = 20_000
17
+ _terminal_buffers: dict[str, deque[str]] = defaultdict(
18
+ lambda: deque(maxlen=_BUFFER_MAX_CHUNKS)
19
+ )
20
+ _terminal_active: set[str] = set()
21
+
22
+
23
+ def _session_id(value: str | None) -> str:
24
+ """Normalize the client-provided diagnostic key without trusting it."""
25
+ value = (value or "default").strip()
26
+ return value[:128] or "default"
27
+
28
+
29
+ def _append_buffer(session_id: str, data: bytes) -> None:
30
+ text = data.decode("utf-8", errors="replace")
31
+ if text:
32
+ _terminal_buffers[session_id].append(text)
33
+ # Keep the joined diagnostic bounded even when chunks are large.
34
+ while sum(len(chunk) for chunk in _terminal_buffers[session_id]) > _BUFFER_MAX_CHARS:
35
+ _terminal_buffers[session_id].popleft()
36
+
37
  # ── Startup script (S755) ─────────────────────────────────────────────────────
38
  # Scritto in /data/.bashrc_agente e sourciate da bash via --rcfile.
39
  # Configura venv Python + npm persistenti, Playwright, workspace, aliases, prompt.
 
265
  'generated_at': int(time.time()),
266
  }
267
 
268
+
269
+ @router.get('/api/terminal/buffer/{session_id}')
270
+ async def terminal_buffer(
271
+ session_id: str,
272
+ role: AuthRole = Depends(require_role(AuthRole.MACHINE)),
273
+ ):
274
+ """Return a bounded recent PTY diagnostic buffer.
275
+
276
+ The route is machine-authenticated because terminal output can contain
277
+ project data. It deliberately exposes no tmux metadata or environment.
278
+ """
279
+ sid = _session_id(session_id)
280
+ return {
281
+ "buffer": "".join(_terminal_buffers.get(sid, ())),
282
+ "active": sid in _terminal_active,
283
+ "session_id": sid,
284
+ }
285
+
286
+
287
  @router.websocket('/ws/terminal')
288
  async def terminal_ws(ws: WebSocket):
289
  """
 
308
  await ws.close(code=4403)
309
  return
310
  await ws.accept()
311
+ _sid = _session_id(ws.query_params.get("session_id"))
312
+ _terminal_active.add(_sid)
313
  loop = asyncio.get_event_loop()
314
 
315
  # S755: assicura che /data/.bashrc_agente esista e sia aggiornato
 
374
  try:
375
  data = await loop.run_in_executor(None, lambda: os.read(master_fd, 4096))
376
  if data:
377
+ _append_buffer(_sid, data)
378
  await ws.send_bytes(data)
379
  # S754-B: salvataggio periodico ogni 60s durante attivitΓ 
380
  _now = time.monotonic()
 
409
  try:
410
  await asyncio.gather(_reader(), _writer())
411
  finally:
412
+ _terminal_active.discard(_sid)
413
  closed.set()
414
  # S754-B: salva lo stato prima di terminare il processo.
415
  # La sessione tmux Γ¨ ancora viva qui (proc Γ¨ il CLIENT tmux, non il SERVER).