feat(ia): IA גל-2 — איחוד-משטחים: ערוץ-למידה אחד · /operations⊇/diagnostics · MET-2/3 (#131, X17)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 9s
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 9s
גל-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>
This commit is contained in:
@@ -1,270 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { AlertTriangle, CheckCircle2, Clock, Database } from "lucide-react";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
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>
|
||||
);
|
||||
}
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
// INV-IA4: /diagnostics was merged into /operations (one monitoring surface).
|
||||
// The system-health cards now render inside /operations as <SystemHealthSection/>.
|
||||
// This redirect keeps old links/bookmarks working.
|
||||
export default function DiagnosticsPage() {
|
||||
const { data, isPending, error } = useDiagnostics();
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
<header>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">אבחון מערכת</span>
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">אבחון מערכת</h1>
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
||||
מצב ה-DB, מסמכים שנכשלו או תקועים, ומשימות רקע פעילות. מתעדכן כל 10
|
||||
שניות.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
{error ? (
|
||||
<Card className="bg-danger-bg border-danger/40">
|
||||
<CardContent className="px-6 py-6 text-center text-danger">
|
||||
{error.message}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
{/* 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 — this surface only points, never decides). */}
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<h2 className="text-navy text-lg 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>
|
||||
</h2>
|
||||
{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">
|
||||
<h2 className="text-navy text-lg 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>
|
||||
</h2>
|
||||
{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">
|
||||
<h2 className="text-danger text-lg 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>
|
||||
</h2>
|
||||
{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">
|
||||
<h2 className="text-warn text-lg mb-3 flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
תקועים (> 10 דק׳)
|
||||
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
|
||||
{data?.stuck_documents.length ?? 0}
|
||||
</Badge>
|
||||
</h2>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
redirect("/operations");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { SystemHealthSection } from "@/components/operations/system-health-section";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -333,17 +334,33 @@ function PipelineCard({
|
||||
title,
|
||||
desc,
|
||||
children,
|
||||
href,
|
||||
hrefLabel,
|
||||
}: {
|
||||
title: string;
|
||||
desc: string;
|
||||
children: React.ReactNode;
|
||||
// INV-IA1: a human-gate card is a *pointer* — the action lives at `href`
|
||||
// (/approvals), never duplicated here. /operations only monitors.
|
||||
href?: string;
|
||||
hrefLabel?: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-5 py-4 space-y-2.5">
|
||||
<div>
|
||||
<h3 className="text-navy text-sm font-semibold mb-0.5">{title}</h3>
|
||||
<p className="text-ink-muted text-[0.72rem]">{desc}</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-navy text-sm font-semibold mb-0.5">{title}</h3>
|
||||
<p className="text-ink-muted text-[0.72rem]">{desc}</p>
|
||||
</div>
|
||||
{href && (
|
||||
<Link
|
||||
href={href}
|
||||
className="shrink-0 text-[0.72rem] text-gold-deep hover:underline whitespace-nowrap"
|
||||
>
|
||||
{hrefLabel ?? "לטיפול ←"}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</CardContent>
|
||||
@@ -619,7 +636,9 @@ export default function OperationsPage() {
|
||||
|
||||
<PipelineCard
|
||||
title="אישור הלכות (שער יו״ר)"
|
||||
desc="הלכות שחולצו, ממתינות להכרעת דפנה ב-/approvals — שער-אנושי, לא תהליך"
|
||||
desc="הלכות שחולצו, ממתינות להכרעת דפנה — שער-אנושי, לא תהליך"
|
||||
href="/approvals"
|
||||
hrefLabel="לתיבת-האישורים ←"
|
||||
>
|
||||
<StatusRow by={data.pipelines.halacha_review.by_status} />
|
||||
</PipelineCard>
|
||||
@@ -627,6 +646,8 @@ export default function OperationsPage() {
|
||||
<PipelineCard
|
||||
title="פסיקה חסרה"
|
||||
desc="פערים בקורפוס — נסגרים אוטומטית כשנקלטים"
|
||||
href="/approvals"
|
||||
hrefLabel="לתיבת-האישורים ←"
|
||||
>
|
||||
<StatusRow by={data.pipelines.missing_precedents.by_status} />
|
||||
</PipelineCard>
|
||||
@@ -688,6 +709,16 @@ export default function OperationsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* INV-IA4: the former /diagnostics surface, folded in here — one
|
||||
monitoring intent, one surface. /diagnostics now redirects here. */}
|
||||
<div className="space-y-3 pt-2">
|
||||
<h2 className="text-navy text-lg mb-0">בריאות-מערכת</h2>
|
||||
<p className="text-ink-muted text-sm">
|
||||
מצב ה-DB, תורי-אישור, ומסמכים שנכשלו/תקועים. (אוחד מ״אבחון״.)
|
||||
</p>
|
||||
<SystemHealthSection />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -71,9 +71,10 @@ const KNOWLEDGE_MENUS: NavMenuDef[] = [
|
||||
|
||||
const ADMIN_ITEMS: NavItem[] = [
|
||||
{ href: "/skills", label: "מיומנויות" },
|
||||
// INV-IA4: /diagnostics folded into /operations (one monitoring surface);
|
||||
// /diagnostics now redirects there, so it's dropped from the nav.
|
||||
{ href: "/operations", label: "תפעול" },
|
||||
{ href: "/scripts", label: "סקריפטים" },
|
||||
{ href: "/diagnostics", label: "אבחון" },
|
||||
{ href: "/settings", label: "הגדרות" },
|
||||
];
|
||||
|
||||
|
||||
255
web-ui/src/components/operations/system-health-section.tsx
Normal file
255
web-ui/src/components/operations/system-health-section.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
"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" />
|
||||
תקועים (> 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>
|
||||
);
|
||||
}
|
||||
@@ -149,11 +149,11 @@ function RecentFindings() {
|
||||
<span className="text-navy font-semibold tabular-nums">
|
||||
{f.decision_number || "—"}
|
||||
</span>
|
||||
{f.applied_to_skill && (
|
||||
{f.review_status === "approved" && (
|
||||
<Badge variant="outline"
|
||||
className="bg-success-bg text-success border-success/40">
|
||||
<CheckCircle2 className="w-3 h-3 me-0.5" />
|
||||
אומץ
|
||||
מאושר
|
||||
</Badge>
|
||||
)}
|
||||
<span className="grow text-ink-muted text-end">
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
* The chair can:
|
||||
* - Add a lesson typed manually (category = "general" by default)
|
||||
* - Edit / delete existing lessons
|
||||
* - Mark a lesson as "applied_to_skill" (informational — doesn't
|
||||
* auto-commit anything to SKILL.md; chair still curates that file
|
||||
* manually in git).
|
||||
* - Approve / reject a lesson (review_status — INV-LRN1/G10): only an
|
||||
* approved lesson flows to the writer. This is the SINGLE writer gate;
|
||||
* the old informative-only applied_to_skill flag was removed (LRN-1/INV-IA3).
|
||||
*
|
||||
* Lessons from the curator arrive with source="curator" and are visually
|
||||
* distinguished by a badge so the chair can audit auto-suggestions.
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Plus, Save, Trash2, Loader2, CheckCircle2, Sparkles, Check, X, Undo2,
|
||||
Plus, Save, Trash2, Loader2, Check, X, Undo2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -191,17 +191,6 @@ function LessonItem({
|
||||
}
|
||||
};
|
||||
|
||||
const onToggleApplied = async () => {
|
||||
try {
|
||||
await patch.mutateAsync({
|
||||
id: lesson.id,
|
||||
patch: { applied_to_skill: !lesson.applied_to_skill },
|
||||
});
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "כשל בעדכון");
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (!window.confirm("למחוק את הלקח?")) return;
|
||||
try {
|
||||
@@ -226,13 +215,6 @@ function LessonItem({
|
||||
<Badge variant="outline" className={reviewBadge.cls}>
|
||||
{reviewBadge.label}
|
||||
</Badge>
|
||||
{lesson.applied_to_skill && (
|
||||
<Badge variant="outline"
|
||||
className="bg-success-bg text-success border-success/40">
|
||||
<CheckCircle2 className="w-3 h-3 me-1" />
|
||||
אומץ
|
||||
</Badge>
|
||||
)}
|
||||
<span className="grow text-ink-muted tabular-nums">
|
||||
{new Date(lesson.created_at).toLocaleDateString("he-IL")}
|
||||
</span>
|
||||
@@ -307,11 +289,6 @@ function LessonItem({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button variant="ghost" size="sm" onClick={onToggleApplied}
|
||||
disabled={patch.isPending}>
|
||||
<Sparkles className="w-3 h-3 me-1" />
|
||||
{lesson.applied_to_skill ? "בטל סימון 'אומץ'" : "סמן כ'אומץ ל-SKILL'"}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setEditing(true)}>
|
||||
ערוך
|
||||
</Button>
|
||||
|
||||
@@ -295,7 +295,8 @@ export type CuratorFinding = {
|
||||
id: string;
|
||||
lesson_text: string;
|
||||
category: string;
|
||||
applied_to_skill: boolean;
|
||||
// review_status replaces the removed applied_to_skill flag (LRN-1/INV-IA3).
|
||||
review_status: "proposed" | "approved" | "rejected";
|
||||
decision_number: string;
|
||||
decision_date: string;
|
||||
created_at: string;
|
||||
@@ -479,9 +480,9 @@ export type DecisionLesson = {
|
||||
// "panel:deepseek+gemini" — the two-judge style panel; (string) keeps it open
|
||||
// to future panel sources without a type break.
|
||||
source: "manual" | "curator" | "chair" | "style_analyzer" | (string & {});
|
||||
applied_to_skill: boolean;
|
||||
// review gate (INV-LRN1/G10): only "approved" lessons flow to the writer.
|
||||
// Panel-written lessons start as "proposed" and wait for the chair here.
|
||||
// (applied_to_skill removed — LRN-1/INV-IA3: review_status is the single gate.)
|
||||
review_status: "proposed" | "approved" | "rejected";
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
@@ -499,7 +500,6 @@ export type LessonCreate = {
|
||||
export type LessonPatch = {
|
||||
lesson_text?: string;
|
||||
category?: DecisionLesson["category"];
|
||||
applied_to_skill?: boolean;
|
||||
review_status?: DecisionLesson["review_status"];
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user