feat(generate): סימון-סטטוס לריצת-הרקע של "הפקת מסמכים" (#227 ג)
עד כה ה-"מייצר…" כיסה רק את שנייה-ה-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:
@@ -29,12 +29,58 @@ export type GenerateTriggerResult = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
/** The two host-side background generation actions (poll target of the status chip). */
|
||||
export type GenerateAction = "party_claims_summary" | "interim_draft";
|
||||
|
||||
export const generateKeys = {
|
||||
all: ["generate"] as const,
|
||||
partyClaimsSummary: (caseNumber: string) =>
|
||||
[...generateKeys.all, "party-claims-summary", caseNumber] as const,
|
||||
status: (caseNumber: string, action: GenerateAction) =>
|
||||
[...generateKeys.all, "status", action, caseNumber] as const,
|
||||
};
|
||||
|
||||
/**
|
||||
* Server-derived background-run status (#227 ג). Reconstructed on the backend
|
||||
* from the CEO child issue + its heartbeat_run + the output artifact — so it is
|
||||
* correct after navigating away and back; it never depends on the browser having
|
||||
* stayed on the page. `started_at`/`finished_at` are server ISO-8601 UTC stamps,
|
||||
* so the client renders a correct elapsed time on return.
|
||||
*/
|
||||
export type GenerateState = "idle" | "queued" | "running" | "done" | "failed";
|
||||
|
||||
export type GenerateStatus = {
|
||||
case_number: string;
|
||||
action: GenerateAction;
|
||||
state: GenerateState;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
output_ready: boolean;
|
||||
detail: string;
|
||||
issue_id: string | null;
|
||||
};
|
||||
|
||||
export function useGenerateStatus(
|
||||
caseNumber: string | undefined,
|
||||
action: GenerateAction,
|
||||
) {
|
||||
return useQuery<GenerateStatus>({
|
||||
queryKey: generateKeys.status(caseNumber ?? "", action),
|
||||
queryFn: ({ signal }) =>
|
||||
apiRequest<GenerateStatus>(
|
||||
`/api/cases/${caseNumber}/generate/${action}/status`,
|
||||
{ signal },
|
||||
),
|
||||
enabled: Boolean(caseNumber),
|
||||
// Poll only while a run is in flight; a terminal state stops the interval.
|
||||
refetchInterval: (query) => {
|
||||
const s = query.state.data?.state;
|
||||
return s === "running" || s === "queued" ? 5_000 : false;
|
||||
},
|
||||
staleTime: 2_000,
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Read: party-claims executive summary (poll target for button 1) ──
|
||||
404 (not generated yet) is a normal "not ready" state, not an error — we
|
||||
coerce it to null so the panel renders the empty/idle state cleanly. */
|
||||
@@ -70,23 +116,33 @@ export function useGeneratePartyClaimsSummary(caseNumber: string) {
|
||||
{ method: "POST" },
|
||||
),
|
||||
onSuccess: () => {
|
||||
// Start polling fresh — the file will appear after the host run completes.
|
||||
// Start polling fresh — the file will appear after the host run completes,
|
||||
// and the status chip should pick up the new queued/running run.
|
||||
qc.invalidateQueries({
|
||||
queryKey: generateKeys.partyClaimsSummary(caseNumber),
|
||||
});
|
||||
qc.invalidateQueries({
|
||||
queryKey: generateKeys.status(caseNumber, "party_claims_summary"),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Trigger: interim ("party-claims") partial draft (button 2) ── */
|
||||
export function useGenerateInterimDraft(caseNumber: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () =>
|
||||
apiRequest<GenerateTriggerResult>(
|
||||
`/api/cases/${caseNumber}/generate/interim-draft`,
|
||||
{ method: "POST" },
|
||||
),
|
||||
// The result lands in the exports list (טיוטת-ביניים-*.docx); useExports
|
||||
// already polls on a 5s interval, so no extra invalidation is needed here.
|
||||
// The result lands in the exports list (טיוטת-ביניים-*.docx, polled by
|
||||
// useExports); re-arm the status chip so it shows the new run immediately.
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: generateKeys.status(caseNumber, "interim_draft"),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user