feat(operations): פאנל בריאות-הסוכנים + הסלמות אחרונות ב-/operations (#218+#222 UI)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 30s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

הטמעת מוקאפ 02e (מאושר Claude-Design X17) — נראות שרת-הצד שנבנתה כ-backend:
- backend: get_recent_escalations() ב-paperclip_client — משחזר הסלמות מהערות
  author_type='system' (severity+reason מנותחים מהגוף, identifier מ-join); Port
  pc_get_recent_escalations (read-only); endpoint GET /api/operations/agents/escalations.
- frontend: useAgentHealth + useRecentEscalations (טיפוסים מקומיים כמו שאר
  operations.ts) + AgentHealthPanel ב-/operations תחת "בריאות-הסוכנים":
  4 stat-tiles (זומבי/תקוע/עובד/רגוע), שורות-בעיה worst-first, וקארד
  הסלמות-אוטומטיות-אחרונות (severity+תיק+סיבה+מתי).

דיוק מול המוקאפ: issue מוסלם עובר לבעלות-חיים→יוצא מרשימת-הבריאות, לכן
שורות-הבריאות = זומבים טרם-הסלמה וקארד-ההסלמות = מטופלים (משלימים).

Invariants: G12 (fetch מהמעטפת, המגע מהשער; UI צורך endpoint דומייני), G2
(מקור-בריאות/הסלמות יחיד). שער-עיצוב: מוקאפ 02e אושר לפני קוד (feedback_claude_design_gate).

בדיקות: 13 pytest (2 חדשות ל-get_recent_escalations: parse severity/reason,
fallback). frontend: tsc --noEmit + eslint נקיים (build מלא ב-CI/deploy —
symlink node_modules שובר next build ב-worktree). api:types לא נדרש (operations.ts
טיפוסים ידניים); ה-endpoint נפרס אטומית עם ה-UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 04:44:50 +00:00
parent a9db657541
commit cf7c918bed
6 changed files with 328 additions and 0 deletions

View File

@@ -143,3 +143,58 @@ def test_get_agent_health_empty(monkeypatch):
monkeypatch.setattr(pc.asyncpg, "connect", _connect)
result = asyncio.run(pc.get_agent_health())
assert result == {"ok": True, "items": [], "counts": {"zombie": 0, "stalled": 0, "working": 0, "idle": 0}}
# ── recent escalations (fake DB) — parses severity + reason from system notes ──
class _FetchConn:
def __init__(self, rows):
self._rows = rows
async def fetch(self, query, *args):
return self._rows
async def close(self):
pass
def test_get_recent_escalations_parses_severity_and_reason(monkeypatch):
from datetime import datetime
rows = [
{
"issue_id": "iss-1",
"identifier": "CMP-141",
"body": "🚨 הסלמה [high] לחיים\n\nזוהתה לולאת-recovery — 4 יקיצות-שחזור; נמסר לחיים.",
"created_at": datetime(2026, 7, 7, 3, 30, 0),
},
{
"issue_id": "iss-2",
"identifier": "CMPA-112",
"body": "🚨 הסלמה [medium] לחיים\n\nזוהתה לולאת-recovery — 2 יקיצות.",
"created_at": datetime(2026, 7, 7, 2, 49, 0),
},
]
async def _connect(_url):
return _FetchConn(rows)
monkeypatch.setattr(pc.asyncpg, "connect", _connect)
result = asyncio.run(pc.get_recent_escalations())
assert result["ok"] is True
a, b = result["items"]
assert a["identifier"] == "CMP-141" and a["severity"] == "high"
assert "4 יקיצות" in a["reason"] and "🚨" not in a["reason"]
assert a["created_at"] == "2026-07-07T03:30:00"
assert b["severity"] == "medium"
def test_get_recent_escalations_defaults_severity_when_untagged(monkeypatch):
rows = [{"issue_id": "x", "identifier": "CMP-9", "body": "🚨 הסלמה לחיים", "created_at": None}]
async def _connect(_url):
return _FetchConn(rows)
monkeypatch.setattr(pc.asyncpg, "connect", _connect)
(item,) = asyncio.run(pc.get_recent_escalations())["items"]
assert item["severity"] == "medium" # fallback
assert item["created_at"] is None