feat(generate): סימון-סטטוס לריצת-הרקע של "הפקת מסמכים" (#227 ג)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 15s

עד כה ה-"מייצר…" כיסה רק את שנייה-ה-POST; אחרי זה לא היה חיווי אם ההפקה
רצה/הצליחה/נכשלה — ריצה שבוטלה נראתה כמו הצלחה. עכשיו צ'יפ-סטטוס ליד כל
כפתור-הפקה: בתור · רץ+שעון-חי · הושלם+מתי · נכשל+סיבה+"נסה שוב".

**נשמר-שרת (דרישת חיים):** הסטטוס נגזר בשרת מ-issue-הילד של ה-CEO +
ה-heartbeat_run האחרון + קיום-הקובץ — לא מזיכרון-הדף. שעון-הריצה מעוגן
ל-started_at של השרת, כך שעזיבה+חזרה לדף מציגות את המצב והזמן הנכונים,
בלי צורך להישאר בדף.

Backend:
- paperclip_client.get_generation_run_status — קורא issue-ילד + latest run
  מ-Paperclip DB (read-only, מאחורי שער-הפלטפורמה G12); dedup לפי כותרת.
- GET /api/cases/{n}/generate/{action}/status — ממפה ל-idle/queued/running/
  done/failed + output_ready + started_at/finished_at (UTC). run שהסתיים בלי
  קובץ = failed (שלא ייראה כהצלחה).

Frontend:
- useGenerateStatus (poll 5s רק כשפעיל; עוצר בטרמינלי) + GenerateStatusChip
  (שעון-ריצה חי מ-started_at; רכיב חסר-מצב). כפתורי-ההפקה מרעננים את
  שאילתת-הסטטוס ב-onSuccess. שורה 3 (החלטה מלאה, סינכרוני) — בלי צ'יפ.

עיצוב: מוקאפ 29-generate-status-indicator אושר ע"י חיים דרך שער Claude Design (X17).
tsc + eslint + py_compile ירוקים. (api:types לריענון post-deploy.)
Invariants: G12 (Paperclip רק בשער), G1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 11:33:41 +00:00
parent 21ad541659
commit b296422d87
6 changed files with 360 additions and 3 deletions

View File

@@ -1384,6 +1384,73 @@ async def wake_ceo_for_action(
}
# Substring of the generation child-issue title per action (see _ceo_action_brief).
GENERATION_TITLE_SUBSTR = {
"party_claims_summary": "סיכום-מנהלים",
"interim_draft": "טיוטת טענות-הצדדים",
}
async def get_generation_run_status(case_number: str, action: str) -> dict:
"""Server-derived run status for a "הפקת מסמכים" generation action (#227 ג).
Reconstructs the state of the most recent generation child issue for
(case, action) from Paperclip — the child issue + its latest heartbeat_run —
so the UI indicator survives navigation/reload (it never depends on the
browser having stayed on the page). Read-only Paperclip DB access, behind the
platform port (G12).
Returns a serializable dict::
{"issue_id", "issue_status", "run_status", "started_at", "finished_at",
"error", "error_code"}
with all timestamps as ISO-8601 UTC strings (or None). ``issue_id`` is None
when no generation child issue exists yet (→ the endpoint reports 'idle').
"""
substr = GENERATION_TITLE_SUBSTR.get(action)
if not substr:
return {"issue_id": None}
def _iso(dt) -> str | None:
return dt.isoformat() if dt is not None else None
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
try:
issue = await conn.fetchrow(
"""SELECT id, status
FROM issues
WHERE title LIKE ('%' || $1 || '%') AND title LIKE ('%' || $2 || '%')
ORDER BY created_at DESC
LIMIT 1""",
case_number, substr,
)
if issue is None:
return {"issue_id": None}
run = await conn.fetchrow(
"""SELECT hr.status, hr.started_at, hr.finished_at, hr.error, hr.error_code
FROM heartbeat_runs hr
JOIN agent_wakeup_requests awr ON hr.wakeup_request_id = awr.id
WHERE awr.payload->>'issueId' = $1
ORDER BY hr.created_at DESC
LIMIT 1""",
str(issue["id"]),
)
finally:
await conn.close()
return {
"issue_id": str(issue["id"]),
"issue_status": issue["status"],
"run_status": run["status"] if run else None,
"started_at": _iso(run["started_at"]) if run else None,
"finished_at": _iso(run["finished_at"]) if run else None,
"error": (run["error"] if run else None) or None,
"error_code": (run["error_code"] if run else None) or None,
}
def _curator_task_brief(task: str, case_number: str, final_filename: str) -> tuple[str, str]:
"""Build the (sub-issue title, description) for a staged final-decision task.