Files
legal-ai/web-ui/src/components/operations/system-health-section.tsx
Chaim 6e69c1dc38
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 9s
feat(ia): IA גל-2 — איחוד-משטחים: ערוץ-למידה אחד · /operations⊇/diagnostics · MET-2/3 (#131, X17)
גל-2 מבקלוג #127 — איחוד-משטחים לפי משטח-היעד של X17. מקיים INV-IA1/IA3/IA4 +
דלתות-הספ (X6 INV-UI7/8, 07-learning §0.4, 00-constitution G2). שומר G10/INV-LRN1
(לא הוסר שום שער-אנושי — רק שער/דגל כפול).

א) תיבת-אישור אחת (INV-IA1): כרטיסי "אישור הלכות"+"פסיקה חסרה" ב-/operations
   מצביעים ל-/approvals (לתיבת-האישורים ←) — /operations מנטר, /approvals מחליט.

ב) ערוץ-למידה אחד (INV-IA3): הוסר applied_to_skill end-to-end —
   - UI: כפתור "סמן כ'אומץ'" + badge "אומץ" ב-lessons-tab; badge ב-curator-portrait.
   - API: LessonPatch, _lesson_to_json, patch call, curator recent_findings (→review_status).
   - db.py: list/add/update_decision_lesson לא בוחרים/כותבים applied_to_skill;
     הפרמטר הוסר. העמודה+אינדקס נשמרים (back-compat, ללא migration), מסומנים DEPRECATED.
   - types: DecisionLesson/LessonPatch/CuratorFinding.
   review_status='approved' = הסטטוס היחיד "זורם-לכותב" (INV-LRN1, #126).

ג) MET-2/3 lost-update (INV-IA3): _append_methodology_override רץ עכשיו בטרנזקציה
   אחת עם SELECT ... FOR UPDATE — אין read-modify-write מתפצל מול עורך-המתודולוגיה
   או promote מקביל. /methodology = העורך-הקנוני; promote מבטל את ה-cache (גל-1 MET-1).

ד) /operations⊇/diagnostics (INV-IA4): גוף /diagnostics חולץ ל-<SystemHealthSection/>
   ומורנדר ב-/operations תחת "בריאות-מערכת". /diagnostics → redirect ל-/operations.
   /diagnostics הוסר מהניווט. משטח-ניטור יחיד.

ה) דלתות-ספ (≥3 מקורות ב-X17, אושר ע"י חיים /goal):
   - X6: INV-UI7 (aggregate=SSoT, mutation מבטל queryKey) + INV-UI8 (render-or-remove, חלקיות).
   - 07-learning §0.4: שער-אחד + טרנזקציה-אחת + applied_to_skill מוסר.
   - 00-constitution G2: תאום-המתודולוגיה כהפרה-ידועה-ממותנת.
   - X17 דלתות-ספ סומנו  קודדו.

בדיקות: py_compile app.py + db.py ✓ · tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109
קיים-מראש). next build נכשל ב-worktree רק בגלל symlink (Turbopack) — Docker/CI תקין.
api:types יתרענן בדפלוי (curator/lessons אינם response-modeled; הטיפוסים יד-כתובים עודכנו).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 21:04:57 +00:00

256 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/*
* System-health monitoring section (INV-IA4): DB health, table counts, the
* halacha review backlog (read-only pointer to /approvals), active background
* tasks, and failed/stuck documents. Extracted from the former /diagnostics
* page so /operations is the single monitoring surface — /diagnostics now
* redirects here (one intent = one surface).
*/
import Link from "next/link";
import { AlertTriangle, CheckCircle2, Clock, Database } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { useDiagnostics, type DiagDoc } from "@/lib/api/system";
const TABLE_LABELS: Record<string, string> = {
cases: "תיקים",
documents: "מסמכים",
document_chunks: "chunks",
style_corpus: "קורפוס סגנון",
style_patterns: "דפוסי סגנון",
};
function formatRelativeTime(iso: string | null) {
if (!iso) return "—";
const then = new Date(iso);
const diffMs = Date.now() - then.getTime();
const min = Math.floor(diffMs / 60000);
if (min < 1) return "עכשיו";
if (min < 60) return `לפני ${min} דקות`;
const hr = Math.floor(min / 60);
if (hr < 24) return `לפני ${hr} שעות`;
const days = Math.floor(hr / 24);
if (days < 30) return `לפני ${days} ימים`;
return then.toLocaleDateString("he-IL");
}
function DocRow({ doc, tone }: { doc: DiagDoc; tone: "danger" | "warn" }) {
const cls =
tone === "danger" ? "bg-danger-bg/60 border-danger/30" : "bg-warn-bg/60 border-warn/30";
return (
<li className={`rounded border px-3 py-2 flex items-center gap-3 text-sm ${cls}`}>
<div className="flex-1 min-w-0">
<div className="text-ink font-medium truncate" title={doc.title}>
{doc.title || "(ללא כותרת)"}
</div>
<div className="text-[0.72rem] text-ink-muted flex gap-3 mt-0.5">
{doc.case_number && (
<Link href={`/cases/${doc.case_number}`} className="hover:text-gold-deep">
ערר {doc.case_number}
</Link>
)}
<span>{formatRelativeTime(doc.created_at)}</span>
</div>
</div>
<Badge variant="outline" className="text-[0.7rem]">{doc.status}</Badge>
</li>
);
}
export function SystemHealthSection() {
const { data, isPending, error } = useDiagnostics();
if (error) {
return (
<Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-6 text-center text-danger">
{error.message}
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
{/* DB status + table counts */}
<div className="grid gap-4 md:grid-cols-[240px_1fr]">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4 flex flex-col items-start gap-2">
<div className="flex items-center gap-2 text-ink-muted text-[0.72rem] uppercase tracking-wider">
<Database className="w-3.5 h-3.5" />
מצב DB
</div>
{isPending ? (
<Skeleton className="h-6 w-24" />
) : data?.db_ok ? (
<div className="flex items-center gap-2 text-success font-semibold">
<CheckCircle2 className="w-5 h-5" />
<span>מחובר</span>
</div>
) : (
<div className="flex items-center gap-2 text-danger font-semibold">
<AlertTriangle className="w-5 h-5" />
<span>מנותק</span>
</div>
)}
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4">
<div className="text-ink-muted text-[0.72rem] uppercase tracking-wider mb-3">
ספירת טבלאות
</div>
<dl className="grid grid-cols-2 md:grid-cols-5 gap-y-2 gap-x-4">
{Object.keys(TABLE_LABELS).map((key) => (
<div key={key} className="space-y-0.5">
<dt className="text-[0.72rem] text-ink-muted">{TABLE_LABELS[key]}</dt>
<dd className="font-display font-bold text-navy text-xl tabular-nums">
{isPending ? "—" : (data?.tables[key] ?? "—")}
</dd>
</div>
))}
</dl>
</CardContent>
</Card>
</div>
{/* Halacha review backlog (ADM-1, INV-IA5): render the human-gate counter
the backend returns; the action lives at /approvals (INV-IA1). */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h3 className="text-navy text-base mb-3 flex items-center gap-2">
<Clock className="w-4 h-4" />
תור אישור הלכות
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{isPending ? "—" : (data?.halacha_backlog?.pending_review ?? 0)}
</Badge>
<Link
href="/approvals"
className="ms-auto text-[0.75rem] text-gold-deep hover:underline"
>
לאישור
</Link>
</h3>
{isPending ? (
<Skeleton className="h-12 w-full" />
) : (
<dl className="grid grid-cols-2 md:grid-cols-4 gap-y-2 gap-x-4">
<div className="space-y-0.5">
<dt className="text-[0.72rem] text-ink-muted">ממתינים (נקי)</dt>
<dd className="font-display font-bold text-navy text-xl tabular-nums">
{data?.halacha_backlog?.pending_clean ?? 0}
</dd>
</div>
<div className="space-y-0.5">
<dt className="text-[0.72rem] text-ink-muted">דורש תיקון</dt>
<dd className="font-display font-bold text-warn text-xl tabular-nums">
{data?.halacha_backlog?.pending_flagged ?? 0}
</dd>
</div>
<div className="space-y-0.5">
<dt className="text-[0.72rem] text-ink-muted">אושרו</dt>
<dd className="font-display font-bold text-success text-xl tabular-nums">
{data?.halacha_backlog?.approved ?? 0}
</dd>
</div>
<div className="space-y-0.5">
<dt className="text-[0.72rem] text-ink-muted">הוותיק ביותר</dt>
<dd className="text-sm text-ink-soft">
{formatRelativeTime(data?.halacha_backlog?.oldest_pending_at ?? null)}
</dd>
</div>
</dl>
)}
</CardContent>
</Card>
{/* Active tasks */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h3 className="text-navy text-base mb-3 flex items-center gap-2">
<Clock className="w-4 h-4" />
משימות רקע פעילות
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{data?.active_tasks.length ?? 0}
</Badge>
</h3>
{isPending ? (
<Skeleton className="h-16 w-full" />
) : data?.active_tasks.length === 0 ? (
<p className="text-ink-muted text-sm">אין משימות פעילות</p>
) : (
<ul className="space-y-2">
{data?.active_tasks.map((t) => (
<li
key={t.task_id}
className="flex items-center gap-3 text-sm rounded bg-rule-soft/60 border border-rule px-3 py-2"
>
<span className="flex-1 text-ink truncate">
{t.filename || t.task_id}
</span>
<Badge variant="outline" className="text-[0.7rem]">
{t.step || t.status}
</Badge>
</li>
))}
</ul>
)}
</CardContent>
</Card>
{/* Failed + stuck docs */}
<div className="grid gap-4 md:grid-cols-2">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h3 className="text-danger text-base mb-3 flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
מסמכים שנכשלו
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{data?.failed_documents.length ?? 0}
</Badge>
</h3>
{isPending ? (
<Skeleton className="h-20 w-full" />
) : data?.failed_documents.length === 0 ? (
<p className="text-ink-muted text-sm">אין כשלונות</p>
) : (
<ul className="space-y-2">
{data?.failed_documents.map((d) => (
<DocRow key={d.id} doc={d} tone="danger" />
))}
</ul>
)}
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h3 className="text-warn text-base mb-3 flex items-center gap-2">
<Clock className="w-4 h-4" />
תקועים (&gt; 10 דק׳)
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{data?.stuck_documents.length ?? 0}
</Badge>
</h3>
{isPending ? (
<Skeleton className="h-20 w-full" />
) : data?.stuck_documents.length === 0 ? (
<p className="text-ink-muted text-sm">אין מסמכים תקועים</p>
) : (
<ul className="space-y-2">
{data?.stuck_documents.map((d) => (
<DocRow key={d.id} doc={d} tone="warn" />
))}
</ul>
)}
</CardContent>
</Card>
</div>
</div>
);
}