feat(ui): chair approval center — one page for every pending human-gate (#63 follow-up)

Dafna asked for a single page under the prod site listing everything she needs
to approve, so nothing is forgotten — the visible embodiment of INV-G10 (human
gates) and INV-QA1 (halacha backlog must be visible).

Backend — GET /api/chair/pending aggregates every pending chair gate, each as a
direct source query (count + sample + action link):
- halachot review backlog (review_status='pending_review') + oldest
- open missing precedents
- unresolved chair_feedback
- qa_failed cases
- gold-set review (FU-5, file-based, best-effort: total vs source='chair')

Frontend — /approvals page ("מרכז אישורים"):
- src/lib/api/chair.ts — usePendingApprovals() (hand-typed until next api:types)
- src/app/approvals/page.tsx — card per category, severity-coloured count, sample
  rows, oldest-pending date, link to where each is handled; live (60s refetch)
- app-shell nav: "מרכז אישורים" in the work group + total-pending badge (quiet at 0)

Live counts at build time surfaced the value immediately: 226 open missing
precedents, 178 pending halachot, 20 unapplied feedback notes, 1 qa_failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 15:36:29 +00:00
parent aee2140b0b
commit 19d3dc81d0
4 changed files with 320 additions and 3 deletions

View File

@@ -0,0 +1,41 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "./client";
/**
* Chair approval center (INV-G10) — aggregates every pending human-gate item
* (halacha approvals, missing precedents, unapplied feedback, QA-failed cases,
* gold-set review) so nothing Dafna must approve is forgotten.
*
* Hand-typed (not from the generated types.ts) because /api/chair/pending is a
* new endpoint; switch to the generated type after the next `npm run api:types`.
*/
export type ApprovalSeverity = "high" | "medium" | "low" | "ok";
export type ApprovalSample = { text: string; source: string };
export type ApprovalCategory = {
key: string;
label: string;
description: string;
count: number;
severity: ApprovalSeverity;
href: string | null;
oldest_at?: string | null;
sample?: ApprovalSample[];
extra?: { total: number; reviewed: number };
};
export type PendingApprovals = {
total_pending: number;
generated_at: string;
categories: ApprovalCategory[];
};
export function usePendingApprovals() {
return useQuery({
queryKey: ["chair", "pending"],
queryFn: () => apiRequest<PendingApprovals>("/api/chair/pending"),
refetchInterval: 60_000,
staleTime: 30_000,
});
}