fix(ia): IA גל-1 — סנכרון-cache + נתונים-שגויים + מחיקת-מתים (#130, X17) #207
@@ -129,6 +129,57 @@ export default function DiagnosticsPage() {
|
||||
</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">
|
||||
|
||||
@@ -45,14 +45,14 @@ export default function MethodologyPage() {
|
||||
<TabsContent value="transitions" className="mt-5">
|
||||
<GenericMethodologyPanel
|
||||
category="transition_phrases"
|
||||
hint="ביטויי-מעבר של דפנה, מקובצים לפי תוצאה. עריכה כאן זורמת לכותב (T15). ערך = רשימת מחרוזות JSON."
|
||||
hint="ביטויי-מעבר של דפנה, מקובצים לפי תוצאה. עריכה כאן זורמת לכותב. ערך = רשימת מחרוזות JSON."
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="antipatterns" className="mt-5">
|
||||
<GenericMethodologyPanel
|
||||
category="anti_patterns"
|
||||
hint="דפוסים שדפנה נמנעת מהם — נמדדים ע״י מדד מרחק-הסגנון (T7) ומוזרקים לכותב. ערך = {regex, note}."
|
||||
hint="דפוסים שדפנה נמנעת מהם — נמדדים ע״י מדד מרחק-הסגנון ומוזרקים לכותב. ערך = {regex, note}."
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -443,8 +443,23 @@ function LiveAgentsPanel() {
|
||||
<h2 className="text-navy text-lg mb-0">סוכנים פעילים</h2>
|
||||
{data ? (
|
||||
<div className="flex items-center gap-2 text-[0.72rem]">
|
||||
<Badge variant="default" className="font-normal">רצים {data.running}</Badge>
|
||||
<Badge variant="secondary" className="font-normal">בתור {data.queued}</Badge>
|
||||
{/* ADM-6 (INV-IA5): counts sum only the companies that loaded.
|
||||
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>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -87,11 +87,23 @@ export function EnvVarRow({
|
||||
</span>
|
||||
)}
|
||||
</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="font-mono text-ink" dir="ltr">
|
||||
{spec.container_value ?? <em className="text-ink-muted">— לא מוגדר —</em>}
|
||||
</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>
|
||||
|
||||
|
||||
@@ -30,6 +30,17 @@ const CHECKLIST_ORDER = [
|
||||
"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 = {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -131,12 +142,19 @@ export function ContentChecklistsPanel() {
|
||||
<Card className="border-rule">
|
||||
<CardContent className="px-5 py-4 space-y-3">
|
||||
<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
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
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 ? "עריכה" : "תצוגה מקדימה"}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
useLibrarySearch, type PracticeArea, type SearchHit,
|
||||
} from "@/lib/api/precedent-library";
|
||||
import { PRACTICE_AREAS, PRECEDENT_LEVELS } from "./practice-area";
|
||||
import { AuthorityBadge } from "./halacha-meta";
|
||||
|
||||
function formatDate(iso: string | null) {
|
||||
if (!iso) return "—";
|
||||
@@ -32,6 +33,10 @@ function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> })
|
||||
{hit.court && <span>· {hit.court}</span>}
|
||||
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</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>
|
||||
</div>
|
||||
<p className="text-navy font-medium text-[0.95rem]" dir="rtl">
|
||||
|
||||
@@ -118,7 +118,15 @@ export function ComparePanel() {
|
||||
const { data: corpus, isPending } = useCorpus();
|
||||
const [a, setA] = 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 (
|
||||
<div className="space-y-6">
|
||||
@@ -133,7 +141,7 @@ export function ComparePanel() {
|
||||
</label>
|
||||
<Select
|
||||
disabled={isPending}
|
||||
value={(slot === "a" ? a : b) ?? ""}
|
||||
value={(slot === "a" ? validA : validB) ?? ""}
|
||||
onValueChange={(v) => (slot === "a" ? setA(v) : setB(v))}
|
||||
dir="rtl"
|
||||
>
|
||||
@@ -155,7 +163,7 @@ export function ComparePanel() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{a && b && a === b && (
|
||||
{validA && validB && validA === validB && (
|
||||
<p className="text-ink-muted text-sm text-center">בחר שתי החלטות שונות</p>
|
||||
)}
|
||||
|
||||
@@ -167,7 +175,7 @@ export function ComparePanel() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{cmp.isPending && a && b && a !== b && (
|
||||
{cmp.isPending && validA && validB && validA !== validB && (
|
||||
<Skeleton className="h-60 w-full" />
|
||||
)}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ function StatsCard() {
|
||||
<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="החלטות שנסקרו" 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="ממוצע ממצאים להחלטה"
|
||||
value={
|
||||
data.decisions_with_findings > 0
|
||||
|
||||
@@ -83,10 +83,13 @@ export function StyleReportPanel() {
|
||||
label="ממוצע להחלטה"
|
||||
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
|
||||
label="דפוסי סגנון"
|
||||
value={String(data.signature_phrases.items.length)}
|
||||
caption={`מתוך ${data.contribution.total_patterns} שחולצו`}
|
||||
caption="דפוסים חוזרים שזוהו"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "./client";
|
||||
import { casesKeys } from "./cases";
|
||||
import { decisionBlocksKeys } from "./decision-blocks";
|
||||
|
||||
export type ExportFile = {
|
||||
filename: string;
|
||||
@@ -80,6 +81,9 @@ export function useExportDocx(caseNumber: string) {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: exportsKeys.list(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.activeDraft(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: () => {
|
||||
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: () => {
|
||||
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 { apiRequest } from "./client";
|
||||
import { trainingKeys } from "./training";
|
||||
import { methodologyKeys } from "./methodology";
|
||||
|
||||
export type DraftFinalPair = {
|
||||
id: string;
|
||||
@@ -111,6 +113,13 @@ export function usePromoteLearning(pairId: string) {
|
||||
),
|
||||
onSuccess: () => {
|
||||
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: () => {
|
||||
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 }) => {
|
||||
qc.invalidateQueries({ queryKey: missingPrecedentKeys.detail(id) });
|
||||
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: () => {
|
||||
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.all });
|
||||
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: () => {
|
||||
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: () => {
|
||||
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;
|
||||
};
|
||||
|
||||
/** 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 = {
|
||||
db_ok: boolean;
|
||||
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[];
|
||||
stuck_documents: DiagDoc[];
|
||||
active_tasks: Array<{
|
||||
|
||||
@@ -172,6 +172,10 @@ export function useDeleteCorpusEntry() {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: trainingKeys.corpus() });
|
||||
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;
|
||||
decisions_with_findings: 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[];
|
||||
};
|
||||
|
||||
|
||||
22
web/app.py
22
web/app.py
@@ -1327,9 +1327,13 @@ async def get_curator_stats():
|
||||
"WHERE source = 'curator'"
|
||||
)
|
||||
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 "
|
||||
"WHERE source = 'curator' AND applied_to_skill = true"
|
||||
"WHERE source = 'curator' AND review_status = 'approved'"
|
||||
)
|
||||
# Last 10 curator findings — newest first
|
||||
recent_rows = await conn.fetch(
|
||||
@@ -1348,7 +1352,7 @@ async def get_curator_stats():
|
||||
"total_findings": total_lessons or 0,
|
||||
"decisions_with_findings": decisions_with_findings or 0,
|
||||
"decisions_total": total_corpus or 0,
|
||||
"findings_applied": applied or 0,
|
||||
"findings_approved": approved or 0,
|
||||
"recent_findings": [
|
||||
{
|
||||
"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}
|
||||
|
||||
|
||||
@app.get("/api/precedent-library/queue/pending")
|
||||
async def precedent_queue_pending(kind: str = "metadata", limit: int = 20):
|
||||
"""Read-only view of the queue. The MCP worker reads this too, but the
|
||||
UI calls it to show 'X ממתינות לעיבוד מקומי' badges."""
|
||||
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)}
|
||||
# PRE-2 (INV-IA1/dead-surface): removed GET /api/precedent-library/queue/pending —
|
||||
# it had zero frontend consumers (its docstring's "the UI calls it" claim was
|
||||
# stale; only /api/digests/queue/pending is consumed). The MCP worker reads the
|
||||
# queue via db.list_pending_extraction_requests directly, not over HTTP.
|
||||
|
||||
|
||||
# ── Digests radar (X12) — secondary discovery layer ─────────────────
|
||||
|
||||
Reference in New Issue
Block a user