feat(ops): /operations — מוני-תור אחידים, "מה רץ עכשיו", וניהול-תהליכים

הדף הציג את התורים באופן לא-אחיד (by_status גולמי), בלי הבחנה בין "ממתין"
(בקלוג: status=pending) ל"בתור" (התור הפעיל: requested_at IS NOT NULL), בלי
הצגת הפריט שרץ כרגע, ובלי שום שליטה בתהליכים.

מה נוסף:
1. כרטיסי-תור אחידים — בתור / ממתין(בקלוג) / בעיבוד / הושלם / נכשל + "רץ עכשיו"
   (citation/case_number של הפריט בעיבוד) לכל drain (אחזור-פסיקה, מטא-דאטה,
   הלכות, יומונים). שערי-אנוש (אישור-הלכות, פסיקה-חסרה) נשארים מוני-סטטוס.
2. פאנל ניהול-תהליכים בסגנון "שירותי Windows":
   - דמון (court-fetch-service/xvfb/chat/reaper): הפעל-מחדש / עצור / הפעל.
   - cron drain: "הרץ עכשיו" (pm2 restart) + מתג הפעל/כבה תזמון.
3. כל תגי-הסטטוס מתורגמים לעברית.

מנגנון:
- הפעל/כבה תזמון = דגל ב-DB (טבלה drain_controls). pm2 cron_restart מחיה תהליך
  שעוצר ב-stop, לכן ה"כיבוי" האמין הוא דגל שכל drain בודק ב-startup (no-op מיידי
  כשכבוי). הקונטיינר כותב/קורא ישירות מ-DB.
- הרץ-עכשיו + restart/stop/start = proxy ל-pm2 דרך endpoint חדש בגשר-המארח
  (court_fetch_service /pm2/control), מאובטח Bearer + whitelist ל-legal-* בלבד.
- יומונים: drain_digests הועבר מ-crontab ל-pm2 (legal-digest-drain.config.cjs)
  כדי שיופיע ויהיה שליט כמו כל drain. drain_halacha_queue.py הובא לבקרת-גרסאות.

Invariants: מקיים G2 (הרחבת /operations + הגשר הקיים, לא מסלול מקביל) ו-G1
(drain_controls = מקור-אמת יחיד לכיבוי, נורמליזציה במקור ולא תיקון-בקריאה).
אין בליעת שגיאות שקטה (הגשר מחזיר {ok,error}; המוטציות מציגות toast).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 08:57:23 +00:00
parent 6647aa92e6
commit 638eef6803
11 changed files with 676 additions and 98 deletions

View File

@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { apiRequest } from "./client";
export type OpsService = {
@@ -10,6 +11,7 @@ export type OpsService = {
memory_bytes: number;
cron: string;
autorestart: boolean;
disabled?: boolean; // cron drain switched off via the dashboard
};
export type CourtFetchJob = {
@@ -28,16 +30,27 @@ export type IngestedRow = {
created_at: string;
};
/** The uniform per-pipeline shape every background drain reports. */
export type PipelineStats = {
pending: number; // backlog: rows not yet processed (status default)
processing: number; // being worked right now
done: number; // completed
failed: number; // terminal failures (court_fetch folds in 'manual')
queued: number; // explicitly enqueued for the next drain run
running_now: string[]; // human labels of the items currently processing
by_status: Record<string, number>; // raw counts, for the curious
};
export type OperationsSnapshot = {
services: OpsService[];
services_error: string | null;
pipelines: {
court_fetch: { by_status: Record<string, number>; recent: CourtFetchJob[] };
metadata_extraction: { by_status: Record<string, number>; queued: number };
halacha_extraction: { by_status: Record<string, number>; queued: number };
court_fetch: PipelineStats & { recent: CourtFetchJob[] };
metadata_extraction: PipelineStats;
halacha_extraction: PipelineStats;
digests: PipelineStats & { total: number; linked: number };
halacha_review: { by_status: Record<string, number> };
missing_precedents: { by_status: Record<string, number> };
digests: { total: number; linked: number };
ingested_recent: IngestedRow[];
};
};
@@ -51,3 +64,42 @@ export function useOperations() {
staleTime: 3000,
});
}
export type ServiceAction = "restart" | "stop" | "start" | "run-now";
/** Control a background service (daemon restart/stop/start, or run a drain now). */
export function useServiceAction() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ name, action }: { name: string; action: ServiceAction }) =>
apiRequest(`/api/operations/services/${name}/${action}`, { method: "POST" }),
onSuccess: (_d, { action }) => {
const labels: Record<ServiceAction, string> = {
"run-now": "הופעל עכשיו",
restart: "הופעל מחדש",
stop: "נעצר",
start: "הופעל",
};
toast.success(labels[action]);
qc.invalidateQueries({ queryKey: ["operations"] });
},
onError: (e) => toast.error(`הפעולה נכשלה: ${String(e)}`),
});
}
/** Switch a cron drain on/off (its "startup type"). */
export function useDrainToggle() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ name, disabled }: { name: string; disabled: boolean }) =>
apiRequest(`/api/operations/drains/${name}/disabled`, {
method: "POST",
body: { disabled },
}),
onSuccess: (_d, { disabled }) => {
toast.success(disabled ? "התזמון כובה" : "התזמון הופעל");
qc.invalidateQueries({ queryKey: ["operations"] });
},
onError: (e) => toast.error(`העדכון נכשל: ${String(e)}`),
});
}