fix(halachot): ספירת-תור אמיתית + עדכון-חי בתגי-הכרעה (#6/#7/#8)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
Lint — undefined names / undefined-names (pull_request) Successful in 11s

המספרים בתגי תור-ההלכות היו תקרות-שאילתה ולא ספירה אמיתית:
- "ממתינות 500" = pendingData.items.length עם limit=500
- "נדחו 1000 / אושרו 1000" = useHalachotByStatus(...,1000) — תקרה 1000
ובלי refetchInterval התגים התעדכנו רק בכניסה לדף.

המקור האמיתי כבר קיים: /api/precedent-library/stats מריץ COUNT(*) אמיתי
(pending=1373, approved=2100). מוסיף לו halachot_rejected + halachot_deferred,
מחבר את תגי-ה-HalachaReviewPanel למקור הזה, ומוסיף polling (30s) כדי שהם
יתעדכנו חי. מסיר את useHalachaCount המיותר.

תור-העבודה עצמו עדיין נטען עד 500 פריטים (cap-עבודה לגיטימי); רק תצוגת
הספירות תוקנה להציג את הסך-האמיתי.

Invariants: מקיים G1 (נרמול-במקור — ספירה אמיתית מ-COUNT(*) במקום len(rows)
מתוקרת בקריאה) ו-G2 (מאחד על מקור-הספירה הקיים, ללא endpoint-ספירה מקביל).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 08:48:10 +00:00
parent 576a4b916b
commit 0c78e30e07
3 changed files with 23 additions and 16 deletions

View File

@@ -6719,6 +6719,12 @@ async def precedent_library_stats() -> dict:
halachot_approved = await conn.fetchval( halachot_approved = await conn.fetchval(
"SELECT COUNT(*) FROM halachot WHERE review_status IN ('approved', 'published')" "SELECT COUNT(*) FROM halachot WHERE review_status IN ('approved', 'published')"
) )
halachot_rejected = await conn.fetchval(
"SELECT COUNT(*) FROM halachot WHERE review_status = 'rejected'"
)
halachot_deferred = await conn.fetchval(
"SELECT COUNT(*) FROM halachot WHERE review_status = 'deferred'"
)
return { return {
"precedents_total": int(total or 0), "precedents_total": int(total or 0),
"by_practice_area": [ "by_practice_area": [
@@ -6732,6 +6738,8 @@ async def precedent_library_stats() -> dict:
"halachot_total": int(halachot_total or 0), "halachot_total": int(halachot_total or 0),
"halachot_pending": int(halachot_pending or 0), "halachot_pending": int(halachot_pending or 0),
"halachot_approved": int(halachot_approved or 0), "halachot_approved": int(halachot_approved or 0),
"halachot_rejected": int(halachot_rejected or 0),
"halachot_deferred": int(halachot_deferred or 0),
} }

View File

@@ -11,7 +11,7 @@ import { CorroborationBadge } from "./corroboration-badge";
import { practiceAreaLabel } from "./practice-area"; import { practiceAreaLabel } from "./practice-area";
import { import {
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot, useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot,
isExtractionFixItem, type Halacha, useLibraryStats, isExtractionFixItem, type Halacha,
} from "@/lib/api/precedent-library"; } from "@/lib/api/precedent-library";
import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta"; import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta";
@@ -979,13 +979,6 @@ function PendingPanel() {
); );
} }
// ─── Count badge for tabs ─────────────────────────────────────────────────────
function useHalachaCount(status: string) {
const { data } = useHalachotByStatus(status, 1000);
return data?.count ?? data?.items.length ?? null;
}
// ─── Main export ────────────────────────────────────────────────────────────── // ─── Main export ──────────────────────────────────────────────────────────────
type Tab = "pending" | "rejected" | "approved"; type Tab = "pending" | "rejected" | "approved";
@@ -998,16 +991,16 @@ const TAB_LABELS: Record<Tab, string> = {
export function HalachaReviewPanel() { export function HalachaReviewPanel() {
const [tab, setTab] = useState<Tab>("pending"); const [tab, setTab] = useState<Tab>("pending");
const { data: pendingData } = useHalachotPending({ limit: 500 });
const rejectedCount = useHalachaCount("rejected");
const approvedCount = useHalachaCount("approved");
const pendingCount = pendingData?.items.length ?? null; // Real COUNT(*) totals from the stats endpoint — not the limit-capped
// /api/halachot len(rows), which pinned these chips at 500/1000/1000 and only
// refreshed on navigation. The stats query polls, so the chips track live (#6/#7/#8).
const { data: stats } = useLibraryStats();
const counts: Record<Tab, number | null> = { const counts: Record<Tab, number | null> = {
pending: pendingCount, pending: stats?.halachot_pending ?? null,
rejected: rejectedCount, rejected: stats?.halachot_rejected ?? null,
approved: approvedCount, approved: stats?.halachot_approved ?? null,
}; };
return ( return (

View File

@@ -188,6 +188,8 @@ export type LibraryStats = {
halachot_total: number; halachot_total: number;
halachot_pending: number; halachot_pending: number;
halachot_approved: number; halachot_approved: number;
halachot_rejected: number;
halachot_deferred: number;
}; };
export type ListFilters = { export type ListFilters = {
@@ -301,7 +303,11 @@ export function useLibraryStats() {
queryKey: libraryKeys.stats(), queryKey: libraryKeys.stats(),
queryFn: ({ signal }) => queryFn: ({ signal }) =>
apiRequest<LibraryStats>("/api/precedent-library/stats", { signal }), apiRequest<LibraryStats>("/api/precedent-library/stats", { signal }),
staleTime: 60_000, staleTime: 30_000,
// The halacha-review tab counts ride on this (real COUNT(*) — not the
// limit-capped /api/halachot len), so poll so the chips track approvals
// live instead of only refreshing on navigation (#6/#7/#8).
refetchInterval: 30_000,
}); });
} }