"use client"; import { useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/app-shell"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Skeleton } from "@/components/ui/skeleton"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { useOperations, useServiceAction, useDrainToggle, useAgentRuns, useRunLog, useCancelRun, useResetAgentSession, type OpsService, type OperationsSnapshot, type PipelineStats, type AgentRun, } from "@/lib/api/operations"; function mb(bytes: number): string { return `${Math.round((bytes || 0) / 1024 / 1024)}MB`; } function ago(ms: number): string { if (!ms) return "—"; const secs = Math.floor((Date.now() - ms) / 1000); if (secs < 60) return `לפני ${secs}ש׳`; if (secs < 3600) return `לפני ${Math.floor(secs / 60)}ד׳`; if (secs < 86400) return `לפני ${Math.floor(secs / 3600)}ש׳`; return `לפני ${Math.floor(secs / 86400)}י׳`; } // Hebrew labels for the raw status strings the backend reports. const STATUS_HE: Record = { online: "פעיל", stopped: "עצור", errored: "שגיאה", launching: "עולה", pending: "ממתין", processing: "בעיבוד", running: "רץ", done: "הושלם", completed: "הושלם", failed: "נכשל", manual: "ידני", approved: "אושר", pending_review: "ממתין לאישור", rejected: "נדחה", published: "פורסם", deferred: "נדחה זמנית", open: "פתוח", closed: "סגור", unknown: "לא ידוע", }; function he(status: string): string { return STATUS_HE[status] ?? status; } const STATUS_VARIANT: Record = { online: "default", done: "default", completed: "default", approved: "default", published: "default", stopped: "secondary", pending: "secondary", pending_review: "secondary", open: "secondary", running: "outline", processing: "outline", launching: "outline", failed: "destructive", errored: "destructive", manual: "destructive", rejected: "destructive", }; function StatusBadge({ value, count }: { value: string; count?: number }) { return ( {he(value)} {count !== undefined ? {count} : null} ); } const SERVICE_LABELS: Record = { "legal-court-fetch-service": "שירות אחזור פסיקה (דפדפן נט המשפט)", "legal-court-fetch-xvfb": "צג וירטואלי (Xvfb) לדפדפן", "legal-court-fetch-drain": "תזמון: ניקוז תור אחזור פסיקה (שעתי)", "legal-metadata-drain": "תזמון: חילוץ מטא-דאטה (Gemini, ×15 דק׳)", "legal-halacha-drain": "תזמון: חילוץ הלכות (Claude, ×שעתיים)", "legal-digest-drain": "תזמון: העשרת יומונים (Sonnet, ×שעתיים)", "legal-reaper": "מנקה תהליכים-יתומים (נגד דליפות זיכרון)", "legal-chat-service": "שירות צ׳אט אימון (גשר ל-claude CLI)", }; // ── Process-management panel (the "Windows services" view) ───────────────── function ServiceControls({ s, busy, onAction, onToggle, }: { s: OpsService; busy: boolean; onAction: (action: "restart" | "stop" | "start" | "run-now") => void; onToggle: (disabled: boolean) => void; }) { const isCron = !!s.cron; if (isCron) { // Cron drain: "run now" (pm2 restart) + an enable/disable switch (DB flag). return (
); } // Daemon: restart / stop / start. const online = s.status === "online"; return (
{online ? ( ) : ( )}
); } function ServicesPanel({ data }: { data: OperationsSnapshot }) { const action = useServiceAction(); const toggle = useDrainToggle(); const busy = action.isPending || toggle.isPending; return (

ניהול תהליכי-רקע (pm2)

כמו "שירותים" ב-Windows. דמון = שירות רץ-תמיד (הפעל-מחדש/עצור/הפעל). תזמון (cron) = רץ לפי לוח-זמנים ("הרץ עכשיו" להרצה מיידית, ומתג הפעלה/כיבוי של התזמון).

{data.services_error ? (

{data.services_error}

) : data.services.length === 0 ? (

אין שירותים.

) : (
{data.services.map((s: OpsService) => { const isCron = !!s.cron; return (
{isCron ? ( {s.disabled ? "כבוי" : "פעיל (מתוזמן)"} ) : ( )} {s.cron ? ( {s.cron} ) : null}
{SERVICE_LABELS[s.name] ?? s.name}
{s.name} {mb(s.memory_bytes)} ↻{s.restarts} {isCron ? `ריצה אחרונה ${ago(s.uptime_ms)}` : ago(s.uptime_ms)}
action.mutate({ name: s.name, action: a })} onToggle={(disabled) => toggle.mutate({ name: s.name, disabled })} />
); })}
)}
); } // ── Uniform queue stats ──────────────────────────────────────────────────── function StatTile({ label, value, tone, title, }: { label: string; value: number; tone: "navy" | "muted" | "amber" | "green" | "red"; title?: string; }) { const toneClass = { navy: "text-navy", muted: "text-ink-muted", amber: "text-gold-deep", green: "text-emerald-600", red: "text-destructive", }[tone]; return (
{value} {label}
); } function UniformStats({ p }: { p: PipelineStats }) { return (
{p.failed > 0 ? : null}
{p.running_now.length > 0 ? (
רץ עכשיו: {p.running_now.join(" · ")}
) : (
אין פריט בעיבוד כרגע
)}
); } function StatusRow({ by }: { by: Record }) { const entries = Object.entries(by).filter(([, n]) => n > 0); if (entries.length === 0) return ריק; return (
{entries .sort((a, b) => b[1] - a[1]) .map(([k, n]) => ( ))}
); } function PipelineCard({ title, desc, children, }: { title: string; desc: string; children: React.ReactNode; }) { return (

{title}

{desc}

{children}
); } // ── Live agents — who's working now + output + controls ──────────────────── // The platform's own liveness signal → a Hebrew label + tone. const SILENCE_HE: Record = { ok: { label: "פעיל", tone: "text-emerald-600" }, suspicion: { label: "שקט חשוד", tone: "text-gold-deep" }, critical: { label: "תקוע?", tone: "text-destructive" }, }; /** Best-effort: turn the captured NDJSON stream into readable lines (tail). */ function parseRunLog(content: string, maxLines = 400): string { if (!content) return ""; const out: string[] = []; for (const raw of content.split("\n")) { const line = raw.trim(); if (!line) continue; let chunk = line; try { const wrap = JSON.parse(line); chunk = typeof wrap.chunk === "string" ? wrap.chunk : line; } catch { // not a wrapper line — keep raw } // The chunk is often a claude stream-json event; extract the human bits. for (const part of chunk.split("\n")) { const p = part.trim(); if (!p) continue; try { const ev = JSON.parse(p); if (ev?.type === "assistant" && ev?.message?.content) { const txt = (ev.message.content as Array<{ type: string; text?: string }>) .filter((c) => c.type === "text" && c.text) .map((c) => c.text) .join(""); if (txt) out.push(txt); } else if (ev?.type === "result" && typeof ev.result === "string") { out.push(`▸ ${ev.result}`); } else if (ev?.type === "system" && ev?.subtype) { out.push(`· [${ev.subtype}]`); } else { out.push(p); } } catch { out.push(p); } } } return out.slice(-maxLines).join("\n"); } function RunLogDialog({ run, onClose }: { run: AgentRun | null; onClose: () => void }) { const { data, isLoading, error } = useRunLog(run?.run_id ?? null); const text = data ? parseRunLog(data.content) : ""; return ( !o && onClose()}> פלט הסוכן — {run?.agent_name} {run?.company_label} · ריצה {run?.run_id?.slice(0, 8)} · מתעדכן חי {isLoading ? ( ) : error ? (

שגיאה בטעינת הלוג: {String(error)}

) : (
              {text || "אין פלט עדיין."}
            
)}
); } function LiveAgentsPanel() { const { data, isLoading } = useAgentRuns(); const cancel = useCancelRun(); const reset = useResetAgentSession(); const [logRun, setLogRun] = useState(null); const busy = cancel.isPending || reset.isPending; return (

סוכנים פעילים

{data ? (
רצים {data.running} בתור {data.queued}
) : null}

מי מבין סוכני-הוועדה עובד כרגע ומה הפלט שלו — כולל עבודה שלא קשורה לתיק (כמו ריקון תור הלכות ע״י ה-CEO). עצירה היא מבוקרת דרך הפלטפורמה (לא kill).

{isLoading || !data ? ( ) : data.runs.length === 0 ? (

אין סוכן פעיל כרגע.

) : (
{data.errors.length > 0 ? (

לא ניתן לטעון חלק מהחברות: {data.errors.join(" · ")}

) : null} {data.runs.map((r) => { const sil = SILENCE_HE[r.silence_level]; const startMs = r.started_at ? Date.parse(r.started_at) : 0; return (
{r.status === "running" ? "רץ" : "בתור"} {r.agent_name} {sil ? ● {sil.label} : null}
{r.company_label} {r.status === "running" && startMs ? החל {ago(startMs)} : null} {r.invocation_source ? ( {r.invocation_source} ) : null} {r.continuation_attempt > 0 ? ניסיון #{r.continuation_attempt + 1} : null}
); })}
)}
setLogRun(null)} />
); } export default function OperationsPage() { const { data, isLoading, error } = useOperations(); return (

תפעול — מה רץ ברקע

כל מה שמוריד ומנתח אוטומטית: שירותי-המארח, משימות-התזמון, ותורי-העבודה. ניתן להפעיל-מחדש / לעצור / להריץ-עכשיו כל תהליך. מתרענן כל 5 שניות.

{error ? ( שגיאה בטעינת מצב התפעול: {String(error)} ) : isLoading || !data ? (
) : ( <>

{data.pipelines.digests.linked}/{data.pipelines.digests.total} מקושרים לפסיקה

אחזורים אחרונים (תור)

{data.pipelines.court_fetch.recent.map((j) => (
{j.citation_raw?.slice(0, 48) || j.case_number_norm} {j.error ? (
{j.error.slice(0, 80)}
) : null}
{(j.tier || "").slice(0, 7)}
))}

נקלטו לאחרונה (מבתי-משפט)

{data.pipelines.ingested_recent.map((r) => (
{r.case_number} {r.created_at?.slice(0, 16).replace("T", " ")}
))} {data.pipelines.ingested_recent.length === 0 ? (

עדיין אין.

) : null}
)}
); }