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:
@@ -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)}`),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user