Files
legal-ai/web-ui/src/components/operations/system-health-section.tsx
Chaim 67c2c43777
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
fix(web-ui): display all human-facing timestamps in Asia/Jerusalem (deterministic)
Storage stays UTC (DB TIMESTAMPTZ, API ISO-UTC) — only the display layer is
localized, and now deterministically: every timestamp renders pinned to
Asia/Jerusalem via a single Intl-based formatter, so SSR (UTC container) and
the browser agree on any runtime. No layout/visible-format change — only the tz.

- New single date formatter web-ui/src/lib/format-date.ts (G2): formatDate /
  formatDateShort / formatDateLong / formatDateTime / formatDateTimeFull /
  formatTime / formatIsoDate + Israel helpers getIsraelYear / israelDayKey /
  israelMidnightMs / israelParts + formatRelative (long/short/tight wording).
- Routed all ad-hoc toLocaleDateString/toLocaleTimeString/toLocaleString +
  hand-rolled new Date(...).get*() / toISOString().slice(0,10) timestamp call
  sites (30 files) through it. Number toLocaleString left untouched.
- Spec: added INV-UI9 to docs/spec/X6 (UTC storage, Asia/Jerusalem display).

Display-only; no layout/design/IA change → Claude Design gate N/A.
Invariants: G2 (single date formatter, no parallel ad-hoc formatting), INV-UI9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:18:09 +00:00

251 lines
9.8 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";
import { formatDateShort, formatRelative } from "@/lib/format-date";
const TABLE_LABELS: Record<string, string> = {
cases: "תיקים",
documents: "מסמכים",
document_chunks: "chunks",
style_corpus: "קורפוס סגנון",
style_patterns: "דפוסי סגנון",
};
function formatRelativeTime(iso: string | null) {
if (!iso) return "—";
const min = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
// Past 30 days, fall back to an absolute Israel-time date.
if (min >= 30 * 24 * 60) return formatDateShort(iso);
return formatRelative(iso, { units: "long" });
}
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>
);
}