Merge pull request 'fix(ia): IA גל-1 — סנכרון-cache + נתונים-שגויים + מחיקת-מתים (#130, X17)' (#207) from worktree-ia-wave1-sync-fixes into main
This commit was merged in pull request #207.
This commit is contained in:
@@ -129,6 +129,57 @@ export default function DiagnosticsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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 */}
|
{/* Active tasks */}
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-6 py-5">
|
<CardContent className="px-6 py-5">
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ export default function MethodologyPage() {
|
|||||||
<TabsContent value="transitions" className="mt-5">
|
<TabsContent value="transitions" className="mt-5">
|
||||||
<GenericMethodologyPanel
|
<GenericMethodologyPanel
|
||||||
category="transition_phrases"
|
category="transition_phrases"
|
||||||
hint="ביטויי-מעבר של דפנה, מקובצים לפי תוצאה. עריכה כאן זורמת לכותב (T15). ערך = רשימת מחרוזות JSON."
|
hint="ביטויי-מעבר של דפנה, מקובצים לפי תוצאה. עריכה כאן זורמת לכותב. ערך = רשימת מחרוזות JSON."
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="antipatterns" className="mt-5">
|
<TabsContent value="antipatterns" className="mt-5">
|
||||||
<GenericMethodologyPanel
|
<GenericMethodologyPanel
|
||||||
category="anti_patterns"
|
category="anti_patterns"
|
||||||
hint="דפוסים שדפנה נמנעת מהם — נמדדים ע״י מדד מרחק-הסגנון (T7) ומוזרקים לכותב. ערך = {regex, note}."
|
hint="דפוסים שדפנה נמנעת מהם — נמדדים ע״י מדד מרחק-הסגנון ומוזרקים לכותב. ערך = {regex, note}."
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -443,8 +443,23 @@ function LiveAgentsPanel() {
|
|||||||
<h2 className="text-navy text-lg mb-0">סוכנים פעילים</h2>
|
<h2 className="text-navy text-lg mb-0">סוכנים פעילים</h2>
|
||||||
{data ? (
|
{data ? (
|
||||||
<div className="flex items-center gap-2 text-[0.72rem]">
|
<div className="flex items-center gap-2 text-[0.72rem]">
|
||||||
<Badge variant="default" className="font-normal">רצים {data.running}</Badge>
|
{/* ADM-6 (INV-IA5): counts sum only the companies that loaded.
|
||||||
<Badge variant="secondary" className="font-normal">בתור {data.queued}</Badge>
|
When a company errored, mark the totals as a floor ("+") so
|
||||||
|
the operator isn't shown a shrunken depth as if complete. */}
|
||||||
|
<Badge variant="default" className="font-normal">
|
||||||
|
רצים {data.running}{data.errors.length > 0 ? "+" : ""}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="secondary" className="font-normal">
|
||||||
|
בתור {data.queued}{data.errors.length > 0 ? "+" : ""}
|
||||||
|
</Badge>
|
||||||
|
{data.errors.length > 0 ? (
|
||||||
|
<span
|
||||||
|
className="text-warn"
|
||||||
|
title={`ספירה חלקית — חברות שלא נטענו: ${data.errors.join(" · ")}`}
|
||||||
|
>
|
||||||
|
⚠ חלקי
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,11 +87,23 @@ export function EnvVarRow({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="text-[0.72rem] text-ink-muted w-20">Container:</span>
|
<span className="text-[0.72rem] text-ink-muted w-20">Container:</span>
|
||||||
<span className="font-mono text-ink" dir="ltr">
|
<span className="font-mono text-ink" dir="ltr">
|
||||||
{spec.container_value ?? <em className="text-ink-muted">— לא מוגדר —</em>}
|
{spec.container_value ?? <em className="text-ink-muted">— לא מוגדר —</em>}
|
||||||
</span>
|
</span>
|
||||||
|
{/* ADM-5 (INV-IA5/INV-IA6): when Coolify ≠ Container the container is
|
||||||
|
running a stale value until a redeploy — say so in plain Hebrew
|
||||||
|
right here, not only via the top "Drift" badge. */}
|
||||||
|
{coolifyAvailable && spec.drift && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[0.7rem] text-warn border-warn/40"
|
||||||
|
title="הערך נשמר ב-Coolify אך הקונטיינר עדיין מריץ את הקודם — נדרש redeploy"
|
||||||
|
>
|
||||||
|
ממתין ל-redeploy
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,17 @@ const CHECKLIST_ORDER = [
|
|||||||
"betterment_levy",
|
"betterment_levy",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// MET-6 (INV-IA6/INV-IA5): which case selects each checklist — mirrors the
|
||||||
|
// server's get_content_checklist() routing (lessons.py:580-622) so the chair
|
||||||
|
// sees *when* a checklist is consumed, not five unlabelled types.
|
||||||
|
const CHECKLIST_APPLIES: Record<string, string> = {
|
||||||
|
licensing_substantive: "ברירת-מחדל — כל ערר רישוי שאינו נופל לקטגוריה אחרת",
|
||||||
|
licensing_threshold: "עררי רישוי בנושא סמכות / סף / סילוק-על-הסף / זכות-ערר",
|
||||||
|
licensing_property: "עררי רישוי בנושא תימוכין קנייניים / בעלות / הסכמת-דיירים",
|
||||||
|
tama38: 'עררים בנושא תמ"א 38 / חיזוק',
|
||||||
|
betterment_levy: "תיקי היטל השבחה (8xxx)",
|
||||||
|
};
|
||||||
|
|
||||||
type ChecklistItem = {
|
type ChecklistItem = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -131,12 +142,19 @@ export function ContentChecklistsPanel() {
|
|||||||
<Card className="border-rule">
|
<Card className="border-rule">
|
||||||
<CardContent className="px-5 py-4 space-y-3">
|
<CardContent className="px-5 py-4 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold text-navy">{current.label}</h3>
|
<div className="min-w-0">
|
||||||
|
<h3 className="text-sm font-semibold text-navy">{current.label}</h3>
|
||||||
|
{CHECKLIST_APPLIES[current.key] && (
|
||||||
|
<p className="text-[0.72rem] text-ink-muted mt-0.5">
|
||||||
|
חל על: {CHECKLIST_APPLIES[current.key]}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setPreview(!preview)}
|
onClick={() => setPreview(!preview)}
|
||||||
className="text-xs"
|
className="text-xs shrink-0"
|
||||||
>
|
>
|
||||||
{preview ? <EyeOff className="w-3.5 h-3.5 ml-1" /> : <Eye className="w-3.5 h-3.5 ml-1" />}
|
{preview ? <EyeOff className="w-3.5 h-3.5 ml-1" /> : <Eye className="w-3.5 h-3.5 ml-1" />}
|
||||||
{preview ? "עריכה" : "תצוגה מקדימה"}
|
{preview ? "עריכה" : "תצוגה מקדימה"}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
useLibrarySearch, type PracticeArea, type SearchHit,
|
useLibrarySearch, type PracticeArea, type SearchHit,
|
||||||
} from "@/lib/api/precedent-library";
|
} from "@/lib/api/precedent-library";
|
||||||
import { PRACTICE_AREAS, PRECEDENT_LEVELS } from "./practice-area";
|
import { PRACTICE_AREAS, PRECEDENT_LEVELS } from "./practice-area";
|
||||||
|
import { AuthorityBadge } from "./halacha-meta";
|
||||||
|
|
||||||
function formatDate(iso: string | null) {
|
function formatDate(iso: string | null) {
|
||||||
if (!iso) return "—";
|
if (!iso) return "—";
|
||||||
@@ -32,6 +33,10 @@ function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> })
|
|||||||
{hit.court && <span>· {hit.court}</span>}
|
{hit.court && <span>· {hit.court}</span>}
|
||||||
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
|
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
|
||||||
{hit.precedent_level && <span>· {hit.precedent_level}</span>}
|
{hit.precedent_level && <span>· {hit.precedent_level}</span>}
|
||||||
|
{/* PRE-3/PRE-5 (INV-IA5): the derived authority (binding/persuasive)
|
||||||
|
rides on the wire but was dropped here — render it as in the review
|
||||||
|
tab so search shows the same provenance everywhere. */}
|
||||||
|
<AuthorityBadge authority={hit.authority} />
|
||||||
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
|
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-navy font-medium text-[0.95rem]" dir="rtl">
|
<p className="text-navy font-medium text-[0.95rem]" dir="rtl">
|
||||||
|
|||||||
@@ -118,7 +118,15 @@ export function ComparePanel() {
|
|||||||
const { data: corpus, isPending } = useCorpus();
|
const { data: corpus, isPending } = useCorpus();
|
||||||
const [a, setA] = useState<string | null>(null);
|
const [a, setA] = useState<string | null>(null);
|
||||||
const [b, setB] = useState<string | null>(null);
|
const [b, setB] = useState<string | null>(null);
|
||||||
const cmp = useCompare(a, b);
|
|
||||||
|
// LRN-6 (INV-IA2): if a selected decision was deleted from the corpus on
|
||||||
|
// another surface, a cached selection would POST a stale id and 404. Derive
|
||||||
|
// the effective selection from the refreshed corpus instead of holding it in
|
||||||
|
// state — a deleted id resolves to null (no effect, no stale POST).
|
||||||
|
const ids = corpus ? new Set(corpus.map((c) => c.id)) : null;
|
||||||
|
const validA = a && (!ids || ids.has(a)) ? a : null;
|
||||||
|
const validB = b && (!ids || ids.has(b)) ? b : null;
|
||||||
|
const cmp = useCompare(validA, validB);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -133,7 +141,7 @@ export function ComparePanel() {
|
|||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
value={(slot === "a" ? a : b) ?? ""}
|
value={(slot === "a" ? validA : validB) ?? ""}
|
||||||
onValueChange={(v) => (slot === "a" ? setA(v) : setB(v))}
|
onValueChange={(v) => (slot === "a" ? setA(v) : setB(v))}
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
>
|
>
|
||||||
@@ -155,7 +163,7 @@ export function ComparePanel() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{a && b && a === b && (
|
{validA && validB && validA === validB && (
|
||||||
<p className="text-ink-muted text-sm text-center">בחר שתי החלטות שונות</p>
|
<p className="text-ink-muted text-sm text-center">בחר שתי החלטות שונות</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -167,7 +175,7 @@ export function ComparePanel() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{cmp.isPending && a && b && a !== b && (
|
{cmp.isPending && validA && validB && validA !== validB && (
|
||||||
<Skeleton className="h-60 w-full" />
|
<Skeleton className="h-60 w-full" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function StatsCard() {
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
<Kpi label="ממצאי curator" value={data.total_findings} icon={<Sparkles className="w-4 h-4" />} />
|
<Kpi label="ממצאי curator" value={data.total_findings} icon={<Sparkles className="w-4 h-4" />} />
|
||||||
<Kpi label="החלטות שנסקרו" value={`${data.decisions_with_findings}/${data.decisions_total}`} icon={<FileText className="w-4 h-4" />} />
|
<Kpi label="החלטות שנסקרו" value={`${data.decisions_with_findings}/${data.decisions_total}`} icon={<FileText className="w-4 h-4" />} />
|
||||||
<Kpi label="ממצאים שאומצו ל-SKILL" value={data.findings_applied} icon={<CheckCircle2 className="w-4 h-4" />} />
|
<Kpi label="ממצאים מאושרים (זורמים לכותב)" value={data.findings_approved} icon={<CheckCircle2 className="w-4 h-4" />} />
|
||||||
<Kpi label="ממוצע ממצאים להחלטה"
|
<Kpi label="ממוצע ממצאים להחלטה"
|
||||||
value={
|
value={
|
||||||
data.decisions_with_findings > 0
|
data.decisions_with_findings > 0
|
||||||
|
|||||||
@@ -83,10 +83,13 @@ export function StyleReportPanel() {
|
|||||||
label="ממוצע להחלטה"
|
label="ממוצע להחלטה"
|
||||||
value={`${(c.avg_chars / 1000).toFixed(1)}K`}
|
value={`${(c.avg_chars / 1000).toFixed(1)}K`}
|
||||||
/>
|
/>
|
||||||
|
{/* LRN-4 (INV-IA5): items.length (style_patterns freq>0) and
|
||||||
|
contribution.total_patterns are independent queries — the old
|
||||||
|
"X מתוך Y" framed a false subset. Show the surfaced count alone. */}
|
||||||
<KPICard
|
<KPICard
|
||||||
label="דפוסי סגנון"
|
label="דפוסי סגנון"
|
||||||
value={String(data.signature_phrases.items.length)}
|
value={String(data.signature_phrases.items.length)}
|
||||||
caption={`מתוך ${data.contribution.total_patterns} שחולצו`}
|
caption="דפוסים חוזרים שזוהו"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { apiRequest } from "./client";
|
import { apiRequest } from "./client";
|
||||||
import { casesKeys } from "./cases";
|
import { casesKeys } from "./cases";
|
||||||
|
import { decisionBlocksKeys } from "./decision-blocks";
|
||||||
|
|
||||||
export type ExportFile = {
|
export type ExportFile = {
|
||||||
filename: string;
|
filename: string;
|
||||||
@@ -80,6 +81,9 @@ export function useExportDocx(caseNumber: string) {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
|
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
|
||||||
qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
|
qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
|
||||||
|
// CAS-2 (INV-IA2): export flips active_draft → source_of_truth='docx';
|
||||||
|
// the block viewer must re-fetch or its sync warning stays hidden.
|
||||||
|
qc.invalidateQueries({ queryKey: decisionBlocksKeys.case(caseNumber) });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -104,6 +108,9 @@ export function useUploadDraft(caseNumber: string) {
|
|||||||
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
|
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
|
||||||
qc.invalidateQueries({ queryKey: exportsKeys.activeDraft(caseNumber) });
|
qc.invalidateQueries({ queryKey: exportsKeys.activeDraft(caseNumber) });
|
||||||
qc.invalidateQueries({ queryKey: exportsKeys.bookmarks(caseNumber) });
|
qc.invalidateQueries({ queryKey: exportsKeys.bookmarks(caseNumber) });
|
||||||
|
// CAS-1 (INV-IA2): uploading a DOCX draft makes it the source-of-truth;
|
||||||
|
// refresh the block viewer so the divergence banner appears immediately.
|
||||||
|
qc.invalidateQueries({ queryKey: decisionBlocksKeys.case(caseNumber) });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ export function useCreateFeedback() {
|
|||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
||||||
|
// APR-4 (INV-IA2): a new unresolved comment raises the chair-pending
|
||||||
|
// aggregate — keep /approvals + the nav badge in sync.
|
||||||
|
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -103,6 +106,9 @@ export function useResolveFeedback() {
|
|||||||
),
|
),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
||||||
|
// APR-1/APR-4 (INV-IA2): resolving a comment lowers the chair-pending
|
||||||
|
// aggregate; refresh /approvals' counter + the nav badge (one source).
|
||||||
|
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { apiRequest } from "./client";
|
import { apiRequest } from "./client";
|
||||||
|
import { trainingKeys } from "./training";
|
||||||
|
import { methodologyKeys } from "./methodology";
|
||||||
|
|
||||||
export type DraftFinalPair = {
|
export type DraftFinalPair = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -111,6 +113,13 @@ export function usePromoteLearning(pairId: string) {
|
|||||||
),
|
),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: learningKeys.all });
|
qc.invalidateQueries({ queryKey: learningKeys.all });
|
||||||
|
// LRN-10 (INV-IA2): promote flips folded lessons' review_status, so the
|
||||||
|
// per-corpus LessonsTab must refetch (lessons key is corpus-scoped —
|
||||||
|
// invalidate the whole "lessons" prefix since promote is pair-scoped).
|
||||||
|
qc.invalidateQueries({ queryKey: [...trainingKeys.all, "lessons"] });
|
||||||
|
// MET-1 (INV-IA2): promote also writes discussion_rules + transition_
|
||||||
|
// phrases['universal'] — /methodology (staleTime 30s) would stay stale.
|
||||||
|
qc.invalidateQueries({ queryKey: methodologyKeys.all });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,10 @@ export function useCreateMissingPrecedent() {
|
|||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
|
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
|
||||||
|
// APR-6 / ADM-3 (INV-IA2): a new open gap raises the chair-pending
|
||||||
|
// aggregate and the /operations missing_precedents counter.
|
||||||
|
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["operations"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -173,6 +177,10 @@ export function useUpdateMissingPrecedent() {
|
|||||||
onSuccess: (_, { id }) => {
|
onSuccess: (_, { id }) => {
|
||||||
qc.invalidateQueries({ queryKey: missingPrecedentKeys.detail(id) });
|
qc.invalidateQueries({ queryKey: missingPrecedentKeys.detail(id) });
|
||||||
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
|
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
|
||||||
|
// APR-6 / ADM-3 (INV-IA2): a status edit (e.g. open→irrelevant) shifts
|
||||||
|
// the chair-pending aggregate and the /operations counter.
|
||||||
|
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["operations"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -187,6 +195,10 @@ export function useDeleteMissingPrecedent() {
|
|||||||
),
|
),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
|
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
|
||||||
|
// APR-6 / ADM-3 (INV-IA2): removing a gap lowers the chair-pending
|
||||||
|
// aggregate and the /operations counter.
|
||||||
|
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["operations"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -256,6 +268,10 @@ export function useUploadMissingPrecedent() {
|
|||||||
qc.invalidateQueries({ queryKey: missingPrecedentKeys.detail(id) });
|
qc.invalidateQueries({ queryKey: missingPrecedentKeys.detail(id) });
|
||||||
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
|
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
|
||||||
qc.invalidateQueries({ queryKey: ["precedent-library"] });
|
qc.invalidateQueries({ queryKey: ["precedent-library"] });
|
||||||
|
// APR-6 / ADM-3 (INV-IA2): uploading closes the gap → chair-pending
|
||||||
|
// aggregate and the /operations missing_precedents counter both drop.
|
||||||
|
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["operations"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -646,6 +646,10 @@ export function useUpdateHalacha() {
|
|||||||
),
|
),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: libraryKeys.all });
|
qc.invalidateQueries({ queryKey: libraryKeys.all });
|
||||||
|
// APR-5 / ADM-2 (INV-IA2): approving/rejecting a halacha changes the
|
||||||
|
// chair-pending aggregate and the /operations halacha_review counter.
|
||||||
|
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["operations"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -666,6 +670,10 @@ export function useBatchReviewHalachot() {
|
|||||||
),
|
),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: libraryKeys.all });
|
qc.invalidateQueries({ queryKey: libraryKeys.all });
|
||||||
|
// APR-5 / ADM-2 (INV-IA2): a batch review shifts the chair-pending
|
||||||
|
// aggregate and the /operations halacha_review counter.
|
||||||
|
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["operations"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,28 @@ export type DiagDoc = {
|
|||||||
created_at: string | null;
|
created_at: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Halacha review backlog (GAP-14 / INV-QA1 / G10) — human-gate visibility. */
|
||||||
|
export type HalachaBacklog = {
|
||||||
|
pending_review: number;
|
||||||
|
pending_clean: number;
|
||||||
|
pending_flagged: number;
|
||||||
|
approved: number;
|
||||||
|
rejected: number;
|
||||||
|
deferred: number;
|
||||||
|
published: number;
|
||||||
|
total: number;
|
||||||
|
reviewed_total: number;
|
||||||
|
oldest_pending_at: string | null;
|
||||||
|
throughput_24h: number;
|
||||||
|
throughput_7d: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type Diagnostics = {
|
export type Diagnostics = {
|
||||||
db_ok: boolean;
|
db_ok: boolean;
|
||||||
tables: Record<string, number | null>;
|
tables: Record<string, number | null>;
|
||||||
|
// ADM-1 (INV-IA5): the backend returns this human-gate counter; render it
|
||||||
|
// rather than silently dropping it. /approvals owns the action (INV-IA1).
|
||||||
|
halacha_backlog: HalachaBacklog;
|
||||||
failed_documents: DiagDoc[];
|
failed_documents: DiagDoc[];
|
||||||
stuck_documents: DiagDoc[];
|
stuck_documents: DiagDoc[];
|
||||||
active_tasks: Array<{
|
active_tasks: Array<{
|
||||||
|
|||||||
@@ -172,6 +172,10 @@ export function useDeleteCorpusEntry() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: trainingKeys.corpus() });
|
qc.invalidateQueries({ queryKey: trainingKeys.corpus() });
|
||||||
qc.invalidateQueries({ queryKey: trainingKeys.report() });
|
qc.invalidateQueries({ queryKey: trainingKeys.report() });
|
||||||
|
// LRN-8 (INV-IA2): deleting a corpus row nulls style_corpus_id on any
|
||||||
|
// chat that referenced it — refresh the conversation list so those
|
||||||
|
// chats no longer show a stale corpus link.
|
||||||
|
qc.invalidateQueries({ queryKey: chatKeys.conversations() });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -301,7 +305,10 @@ export type CuratorStats = {
|
|||||||
total_findings: number;
|
total_findings: number;
|
||||||
decisions_with_findings: number;
|
decisions_with_findings: number;
|
||||||
decisions_total: number;
|
decisions_total: number;
|
||||||
findings_applied: number;
|
// LRN-5 (INV-IA5): the count of curator lessons with review_status='approved'
|
||||||
|
// (the real INV-LRN1 writer gate) — replaces the old findings_applied, which
|
||||||
|
// counted the informative-only applied_to_skill flag.
|
||||||
|
findings_approved: number;
|
||||||
recent_findings: CuratorFinding[];
|
recent_findings: CuratorFinding[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
22
web/app.py
22
web/app.py
@@ -1327,9 +1327,13 @@ async def get_curator_stats():
|
|||||||
"WHERE source = 'curator'"
|
"WHERE source = 'curator'"
|
||||||
)
|
)
|
||||||
total_corpus = await conn.fetchval("SELECT count(*) FROM style_corpus")
|
total_corpus = await conn.fetchval("SELECT count(*) FROM style_corpus")
|
||||||
applied = await conn.fetchval(
|
# LRN-5 (INV-IA5): count the *real* consumer-mapped gate — review_status
|
||||||
|
# 'approved' is what flows to the writer (INV-LRN1, #126). The old count
|
||||||
|
# of applied_to_skill was a KPI over an informative-only flag (LRN-1)
|
||||||
|
# that writes nowhere, so it reported adoption that never happened.
|
||||||
|
approved = await conn.fetchval(
|
||||||
"SELECT count(*) FROM decision_lessons "
|
"SELECT count(*) FROM decision_lessons "
|
||||||
"WHERE source = 'curator' AND applied_to_skill = true"
|
"WHERE source = 'curator' AND review_status = 'approved'"
|
||||||
)
|
)
|
||||||
# Last 10 curator findings — newest first
|
# Last 10 curator findings — newest first
|
||||||
recent_rows = await conn.fetch(
|
recent_rows = await conn.fetch(
|
||||||
@@ -1348,7 +1352,7 @@ async def get_curator_stats():
|
|||||||
"total_findings": total_lessons or 0,
|
"total_findings": total_lessons or 0,
|
||||||
"decisions_with_findings": decisions_with_findings or 0,
|
"decisions_with_findings": decisions_with_findings or 0,
|
||||||
"decisions_total": total_corpus or 0,
|
"decisions_total": total_corpus or 0,
|
||||||
"findings_applied": applied or 0,
|
"findings_approved": approved or 0,
|
||||||
"recent_findings": [
|
"recent_findings": [
|
||||||
{
|
{
|
||||||
"id": str(r["id"]),
|
"id": str(r["id"]),
|
||||||
@@ -6344,14 +6348,10 @@ async def precedent_request_halachot(case_law_id: str):
|
|||||||
return {"queued": True, "case_law_id": case_law_id, "kind": "halacha", "wakeup": wakeup}
|
return {"queued": True, "case_law_id": case_law_id, "kind": "halacha", "wakeup": wakeup}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/precedent-library/queue/pending")
|
# PRE-2 (INV-IA1/dead-surface): removed GET /api/precedent-library/queue/pending —
|
||||||
async def precedent_queue_pending(kind: str = "metadata", limit: int = 20):
|
# it had zero frontend consumers (its docstring's "the UI calls it" claim was
|
||||||
"""Read-only view of the queue. The MCP worker reads this too, but the
|
# stale; only /api/digests/queue/pending is consumed). The MCP worker reads the
|
||||||
UI calls it to show 'X ממתינות לעיבוד מקומי' badges."""
|
# queue via db.list_pending_extraction_requests directly, not over HTTP.
|
||||||
if kind not in {"metadata", "halacha"}:
|
|
||||||
raise HTTPException(400, "kind חייב להיות metadata או halacha")
|
|
||||||
items = await db.list_pending_extraction_requests(kind=kind, limit=limit)
|
|
||||||
return {"items": items, "count": len(items)}
|
|
||||||
|
|
||||||
|
|
||||||
# ── Digests radar (X12) — secondary discovery layer ─────────────────
|
# ── Digests radar (X12) — secondary discovery layer ─────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user