From 5d75d36e2a1eb149c543489b1f82f02294412800 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 14 Jun 2026 15:27:36 +0000 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=D7=A4=D7=90=D7=A0=D7=9C=20=D7=90?= =?UTF-8?q?=D7=99=D7=A9=D7=95=D7=A8-=D7=AA=D7=9B=D7=A0=D7=99=D7=95=D7=AA?= =?UTF-8?q?=20=E2=80=94=20=D7=98=D7=90=D7=91=20/precedents=20+=20=D7=9E?= =?UTF-8?q?=D7=A8=D7=9B=D7=96-=D7=90=D7=99=D7=A9=D7=95=D7=A8=D7=99=D7=9D?= =?UTF-8?q?=20(PR-B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit הטמעת ה-UI למרשם-התכניות אחרי אישור-עיצוב ב-Claude Design (מוקאפ 22-plans-review). - web-ui/src/lib/api/plans.ts: hooks (usePlansPending, usePlanDuplicates, useUpsertPlan, useUpdatePlan, useReviewPlan, useMergePlans) + טיפוס Plan מקומי. - plans-review-panel.tsx: כרטיס-תכנית עם משפט-הציטוט הקנוני (כפי שייכתב בבלוק ט), שדות-תוקף, סימון חוסר-תאריך, באנר "כפילות אפשרית → מזג לכאן" (find_similar_plans, מיזוג ידני — G10), עריכה/הוספה inline עם תצוגה-מקדימה חיה של הציטוט. - precedents/page.tsx: טאב "תכניות" + PlansPendingPill + deep-link tab=plans. - web/app.py: href קטגוריית-התכניות במרכז-האישורים → /precedents?tab=plans. - api:types: types.ts מחודש מ-openapi החי (5 נתיבי /api/plans). מרכז-האישורים (/approvals) מרנדר קטגוריות גנרית — קטגוריית-התכניות (PR-A) מופיעה אוטומטית. אימות: api:types ✓, tsc --noEmit ✓, lint exit=0 (ללא אזהרות חדשות). Invariants: G10 (אישור אנושי + מיזוג ידני) · INV-AH (ציטוט דטרמיניסטי, תצוגה-מקדימה תואמת format_plan_citation) · INV-IA (שער-אחד: טאב קיים + מרכז-אישורים, ללא עמוד חדש) · X6 (UI↔API, apiRequest + טיפוסים). עבר שער-העיצוב Claude Design (feedback_claude_design_gate). Co-Authored-By: Claude Opus 4.8 (1M context) --- web-ui/src/app/precedents/page.tsx | 14 +- .../precedents/plans-review-panel.tsx | 358 +++++++++++++ web-ui/src/lib/api/plans.ts | 152 ++++++ web-ui/src/lib/api/types.ts | 504 +++++++++++++++++- web/app.py | 2 +- 5 files changed, 1025 insertions(+), 5 deletions(-) create mode 100644 web-ui/src/components/precedents/plans-review-panel.tsx create mode 100644 web-ui/src/lib/api/plans.ts diff --git a/web-ui/src/app/precedents/page.tsx b/web-ui/src/app/precedents/page.tsx index 460a20c..edc3e0f 100644 --- a/web-ui/src/app/precedents/page.tsx +++ b/web-ui/src/app/precedents/page.tsx @@ -7,8 +7,10 @@ import { AppShell } from "@/components/app-shell"; import { LibraryListPanel } from "@/components/precedents/library-list-panel"; import { LibrarySearchPanel } from "@/components/precedents/library-search-panel"; import { HalachaReviewPanel } from "@/components/precedents/halacha-review-panel"; +import { PlansReviewPanel } from "@/components/precedents/plans-review-panel"; import { LibraryStatsPanel } from "@/components/precedents/library-stats-panel"; import { useHalachotPending } from "@/lib/api/precedent-library"; +import { usePlansPending } from "@/lib/api/plans"; import { useMissingPrecedents } from "@/lib/api/missing-precedents"; /** @@ -47,13 +49,18 @@ function PendingPill() { return ; } +function PlansPendingPill() { + const { data } = usePlansPending(); + return ; +} + function IncomingPill() { // "פסיקה נכנסת" = open missing-precedents waiting for the chair to upload. const { data } = useMissingPrecedents({ status: "open", limit: 1 }); return ; } -const PRECEDENT_TABS = new Set(["library", "search", "review", "incoming", "stats"]); +const PRECEDENT_TABS = new Set(["library", "search", "review", "plans", "incoming", "stats"]); export default function PrecedentsPage() { // Controlled so a deep link like /precedents?tab=review (e.g. from a pending @@ -92,6 +99,7 @@ export default function PrecedentsPage() { { value: "library", label: "ספרייה", pill: null }, { value: "search", label: "חיפוש בקורפוס", pill: null }, { value: "review", label: "תור הלכות", pill: }, + { value: "plans", label: "תכניות", pill: }, { value: "incoming", label: "פסיקה נכנסת", @@ -123,6 +131,10 @@ export default function PrecedentsPage() { + + + + {/* "פסיקה נכנסת" — the incoming/missing-precedent queue. Kept as a tab per the mockup; full management lives on /missing-precedents. */} diff --git a/web-ui/src/components/precedents/plans-review-panel.tsx b/web-ui/src/components/precedents/plans-review-panel.tsx new file mode 100644 index 0000000..ee81fc6 --- /dev/null +++ b/web-ui/src/components/precedents/plans-review-panel.tsx @@ -0,0 +1,358 @@ +"use client"; + +import { useState } from "react"; +import { Check, X, Edit2, AlertTriangle, Plus, GitMerge, Save } from "lucide-react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + usePlansPending, usePlanDuplicates, useUpsertPlan, useUpdatePlan, + useReviewPlan, useMergePlans, type Plan, type PlanEdit, +} from "@/lib/api/plans"; + +/* Strip bidi marks (mirror of the halacha panel's cleanCitation). */ +function clean(s: string | null | undefined): string { + if (!s) return "—"; + return s.replace(/[‎‏‪-‮⁦-⁩]/g, "").trim(); +} + +function fmtDate(iso: string | null | undefined): string { + if (!iso) return "—"; + const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso); + if (!m) return iso; + return `${Number(m[3])}.${Number(m[2])}.${m[1]}`; +} + +type EditForm = { + plan_number: string; + display_name: string; + plan_type: string; + gazette_date: string; + yalkut_number: string; + purpose: string; +}; + +const BLANK: EditForm = { + plan_number: "", display_name: "", plan_type: "", + gazette_date: "", yalkut_number: "", purpose: "", +}; + +function toForm(p: Plan): EditForm { + return { + plan_number: p.plan_number, + display_name: p.display_name, + plan_type: p.plan_type, + gazette_date: p.gazette_date ?? "", + yalkut_number: p.yalkut_number, + purpose: p.purpose, + }; +} + +/* Client-side mirror of db.format_plan_citation — deterministic preview so the + * chair sees exactly what block-tet will cite (validity is never free-typed). */ +function previewCitation(f: EditForm): string { + const name = f.display_name.trim() || f.plan_number.trim(); + if (!name) return "—"; + let s = name; + const d = fmtDate(f.gazette_date); + if (d !== "—") { + s = `${name} פורסמה למתן תוקף ברשומות ביום ${d}`; + if (f.yalkut_number.trim()) s += `, י"פ ${f.yalkut_number.trim()}`; + } + if (f.purpose.trim()) s += ` — ${f.purpose.trim()}`; + return s.endsWith(".") ? s : s + "."; +} + +// ───────────────────────────────────────────────────────────────────────────── + +export function PlansReviewPanel() { + const { data, isLoading, isError } = usePlansPending(); + const [adding, setAdding] = useState(false); + + if (isLoading) { + return ( +
+ {[0, 1, 2].map((i) => )} +
+ ); + } + if (isError) { + return

שגיאה בטעינת תור-התכניות.

; + } + + const plans = data?.items ?? []; + + return ( +
+
+

+ מרשם-התכניות: זהות + תוקף קנוניים של כל תב"ע, בשימוש חוזר בין תיקים. + רק תכנית מאושרת מצוטטת בבלוק ט; התוקף נכתב בנוסח אחיד{" "} + דטרמיניסטי — + תאריך-הרשומות ומס' הילקוט לעולם אינם מומצאים. +

+ +
+ + {adding && ( + setAdding(false)} + /> + )} + + {plans.length === 0 ? ( +
+ אין תכניות הממתינות לאישור. +
+ ) : ( + plans.map((p) => ) + )} +
+ ); +} + +// ─── one plan card ──────────────────────────────────────────────────────────── + +function PlanCard({ plan }: { plan: Plan }) { + const [editing, setEditing] = useState(false); + const review = useReviewPlan(); + const merge = useMergePlans(); + const { data: dups } = usePlanDuplicates(plan.id); + const duplicates = dups?.items ?? []; + + const noDate = !plan.gazette_date; + + async function decide(status: "approved" | "rejected") { + try { + await review.mutateAsync({ id: plan.id, status }); + toast.success(status === "approved" ? "התכנית אושרה" : "התכנית נדחתה"); + } catch { + toast.error("שגיאה בעדכון התכנית"); + } + } + + async function foldIn(sourceId: string, label: string) { + try { + await merge.mutateAsync({ sourceId, targetId: plan.id }); + toast.success(`מוזג: ${label} → ${plan.display_name || plan.plan_number}`); + } catch { + toast.error("שגיאה במיזוג"); + } + } + + if (editing) { + return ( + setEditing(false)} + /> + ); + } + + return ( +
+
+ תכנית + + {plan.display_name || plan.plan_number} + + {plan.plan_type && ( + + {plan.plan_type} + + )} + {plan.source_case_number && ( + + מקור: {plan.source_case_number} + + )} +
+ + {duplicates.length > 0 && ( +
+
+ + + כפילות אפשרית — ייתכן שאותה תכנית כבר במרשם בצורה אחרת. מזג כדי לא + לפצל תכנית אחת לשתי רשומות. + +
+ {duplicates.map((d) => ( +
+ + {d.display_name || d.plan_number} + + {d.match_reason && ( + ({d.match_reason}) + )} + +
+ ))} +
+ )} + +
+
+ משפט-הציטוט הקנוני (כפי שייכתב בבלוק ט) +
+
+ {clean(plan.citation_formatted)} +
+
+ +
+
תאריך רשומות: {fmtDate(plan.gazette_date)}
+
ילקוט פרסומים: {plan.yalkut_number || "—"}
+ {plan.plan_type &&
סוג: {plan.plan_type}
} +
+ + {noDate && ( +
+ ⚠ חסר תאריך-תוקף — תצוטט ללא תוקף עד השלמה. +
+ )} + +
+ + + +
+
+ ); +} + +// ─── add / edit form (inline) ─────────────────────────────────────────────── + +function PlanForm({ + title, subtitle, initial, mode, planId, onClose, +}: { + title: string; + subtitle: string; + initial: EditForm; + mode: "add" | "edit"; + planId?: string; + onClose: () => void; +}) { + const [f, setF] = useState(initial); + const upsert = useUpsertPlan(); + const update = useUpdatePlan(); + const busy = upsert.isPending || update.isPending; + + function set(k: K, v: string) { + setF((prev) => ({ ...prev, [k]: v })); + } + + async function save() { + if (!f.plan_number.trim()) { + toast.error("חובה מספר-תכנית"); + return; + } + try { + if (mode === "edit" && planId) { + const patch: PlanEdit = { + plan_number: f.plan_number, display_name: f.display_name, + plan_type: f.plan_type, gazette_date: f.gazette_date, + yalkut_number: f.yalkut_number, purpose: f.purpose, + }; + await update.mutateAsync({ id: planId, patch }); + } else { + await upsert.mutateAsync({ + plan_number: f.plan_number, display_name: f.display_name, + plan_type: f.plan_type, gazette_date: f.gazette_date, + yalkut_number: f.yalkut_number, purpose: f.purpose, + review_status: "approved", + }); + } + toast.success(mode === "edit" ? "התכנית נשמרה" : "התכנית נוספה"); + onClose(); + } catch (e) { + // surface the backend's collision/validation message (no silent failure) + const msg = (e as { body?: { detail?: string } })?.body?.detail; + toast.error(msg || "שגיאה בשמירה"); + } + } + + const field = ( + k: keyof EditForm, label: string, hint?: string, full = false, + ) => ( +
+ + set(k, e.target.value)} /> + {hint &&

{hint}

} +
+ ); + + return ( +
+
+

{title}

+

{subtitle}

+
+
+ {field("plan_number", "מספר תכנית", 'מזהה בלבד (ללא המילה "תכנית")')} + {field("display_name", "שם תצוגה")} + {field("gazette_date", "תאריך פרסום ברשומות", "YYYY-MM-DD")} + {field("yalkut_number", 'מס\' ילקוט פרסומים (י"פ)')} + {field("plan_type", "סוג תכנית", "ארצית / מחוזית / מקומית / מפורטת / כוללנית")} + {field("purpose", "ייעוד (משפט אחד)", undefined, true)} +
+
+ + תצוגה מקדימה — כך ייכתב בבלוק ט: + + {previewCitation(f)} +
+
+ + +
+
+ ); +} diff --git a/web-ui/src/lib/api/plans.ts b/web-ui/src/lib/api/plans.ts new file mode 100644 index 0000000..48e8d8e --- /dev/null +++ b/web-ui/src/lib/api/plans.ts @@ -0,0 +1,152 @@ +/** + * Planning-schemes registry (מרשם-התכניות, V38) — typed API hooks. + * + * SSOT for a plan's identity + validity, reused across cases. LLM-extracted rows + * arrive pending_review; only approved validity feeds block-tet (INV-DM5/G10). + * Variant duplicates are surfaced (usePlanDuplicates), never auto-merged — the + * chair merges manually. Mirrors the precedent-library hook conventions. + */ + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { apiRequest } from "./client"; + +export type PlanReviewStatus = "pending_review" | "approved" | "rejected"; + +export type PlanDiscrepancy = { + field: string; + old: string; + new: string; + source_case_number?: string; + via?: string; +}; + +export type Plan = { + id: string; + plan_number: string; + display_name: string; + aliases: string[]; + plan_type: string; + gazette_date: string | null; // ISO YYYY-MM-DD + yalkut_number: string; + purpose: string; + citation_formatted: string; // the deterministic block-tet sentence + review_status: PlanReviewStatus; + source_case_number: string; + source_document_id: string | null; + model_used: string; + discrepancies: PlanDiscrepancy[]; + created_at: string; + updated_at: string; + /** Only present on a duplicate-candidate hit. */ + match_reason?: string; +}; + +export const planKeys = { + all: ["plans"] as const, + pending: () => [...planKeys.all, "pending"] as const, + duplicates: (id: string) => [...planKeys.all, "duplicates", id] as const, +}; + +/** All plans awaiting the chair gate (G10). */ +export function usePlansPending(limit = 500) { + return useQuery({ + queryKey: planKeys.pending(), + queryFn: ({ signal }) => + apiRequest<{ items: Plan[]; count: number }>( + `/api/plans?review_status=pending_review&limit=${limit}`, + { signal }, + ), + staleTime: 5_000, + refetchOnMount: "always", + }); +} + +/** Near-duplicate candidates for a plan — surfaced for manual merge. */ +export function usePlanDuplicates(planId: string, enabled = true) { + return useQuery({ + queryKey: planKeys.duplicates(planId), + queryFn: ({ signal }) => + apiRequest<{ items: Plan[]; count: number }>( + `/api/plans/${encodeURIComponent(planId)}/duplicates`, + { signal }, + ), + enabled: enabled && !!planId, + staleTime: 10_000, + }); +} + +export type PlanUpsert = { + plan_number: string; + display_name?: string; + plan_type?: string; + gazette_date?: string; // ISO; "" = unknown + yalkut_number?: string; + purpose?: string; + review_status?: PlanReviewStatus; + aliases?: string[]; +}; + +export type PlanEdit = { + plan_number: string; + display_name?: string; + plan_type?: string; + gazette_date?: string; + yalkut_number?: string; + purpose?: string; + aliases?: string[] | null; +}; + +function invalidate(qc: ReturnType) { + qc.invalidateQueries({ queryKey: planKeys.all }); + // the chair-pending aggregate (/approvals) carries a plans count (INV-IA2) + qc.invalidateQueries({ queryKey: ["chair", "pending"] }); +} + +/** Manual chair add/upsert (idempotent on normalized plan_number). */ +export function useUpsertPlan() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: PlanUpsert) => + apiRequest(`/api/plans`, { method: "POST", body }), + onSuccess: () => invalidate(qc), + }); +} + +/** Edit/fix a plan by id (refuses a number collision → use merge). */ +export function useUpdatePlan() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, patch }: { id: string; patch: PlanEdit }) => + apiRequest( + `/api/plans/${encodeURIComponent(id)}`, + { method: "PATCH", body: patch }, + ), + onSuccess: () => invalidate(qc), + }); +} + +/** Chair gate (G10): approve / reject / reset. */ +export function useReviewPlan() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, status }: { id: string; status: PlanReviewStatus }) => + apiRequest( + `/api/plans/${encodeURIComponent(id)}/review`, + { method: "POST", body: { review_status: status } }, + ), + onSuccess: () => invalidate(qc), + }); +} + +/** Fold a source plan into a target (manual dedup); source is deleted. */ +export function useMergePlans() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ sourceId, targetId }: { sourceId: string; targetId: string }) => + apiRequest( + `/api/plans/merge`, + { method: "POST", body: { source_id: sourceId, target_id: targetId } }, + ), + onSuccess: () => invalidate(qc), + }); +} diff --git a/web-ui/src/lib/api/types.ts b/web-ui/src/lib/api/types.ts index fe4942c..8cbd229 100644 --- a/web-ui/src/lib/api/types.ts +++ b/web-ui/src/lib/api/types.ts @@ -2947,9 +2947,11 @@ export interface paths { * Operations Drain Toggle * @description Switch a cron drain on/off (the 'startup type' in the services panel). * - * Written straight to drain_controls — no host roundtrip; the drain reads the - * flag at startup and no-ops when disabled (pm2 cron_restart can't be trusted - * to stay stopped). + * Written to drain_controls so the drain no-ops at its NEXT startup (pm2 + * cron_restart can't be trusted to stay stopped). On disable we ALSO stop any + * currently-running process immediately via the host pm2 bridge — the DB flag + * alone wouldn't halt a drain mid-run. Best-effort: a bridge failure doesn't + * fail the toggle (the supervisor stops it on its next tick as a backstop). */ post: operations["operations_drain_toggle_api_operations_drains__name__disabled_post"]; delete?: never; @@ -3087,6 +3089,33 @@ export interface paths { patch?: never; trace?: never; }; + "/api/operations/agents/migrate-adapter": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Operations Agent Migrate Adapter + * @description Migrate an agent (or 'all') to a target adapter, in both companies. + * + * The migration is host-side (it needs the host filesystem — generated + * instruction copies, the gemini settings file — and the embedded board DB), + * so this proxies scripts/migrate_agent_adapter.py through the court-fetch host + * bridge, Bearer-authenticated exactly like the pm2 controls. The script's exit + * code + stdout/stderr are relayed verbatim so the dashboard can show preflight + * warnings (a non-zero --check is a refusal to render, not a transport error). + */ + post: operations["operations_agent_migrate_adapter_api_operations_agents_migrate_adapter_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/digests/{digest_id}": { parameters: { query?: never; @@ -3428,6 +3457,111 @@ export interface paths { patch?: never; trace?: never; }; + "/api/plans": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Plans List + * @description List the plans registry; filter by review_status (queue) or fuzzy q (search). + */ + get: operations["plans_list_api_plans_get"]; + put?: never; + /** + * Plan Create + * @description Manual chair add/upsert (idempotent on normalized plan_number). + */ + post: operations["plan_create_api_plans_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/plans/{plan_id}/duplicates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Plan Duplicates + * @description Near-duplicate candidates for a plan — for the chair to merge (G10, no auto-merge). + */ + get: operations["plan_duplicates_api_plans__plan_id__duplicates_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/plans/{plan_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Plan Get */ + get: operations["plan_get_api_plans__plan_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Plan Edit + * @description Edit/fix a plan by id (chair). Refuses a number collision (→ merge instead). + */ + patch: operations["plan_edit_api_plans__plan_id__patch"]; + trace?: never; + }; + "/api/plans/{plan_id}/review": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Plan Review + * @description Chair gate (G10): approve / reject / reset. + */ + post: operations["plan_review_api_plans__plan_id__review_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/plans/merge": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Plan Merge + * @description Fold source plan into target (chair-initiated dedup); source is deleted. + */ + post: operations["plan_merge_api_plans_merge_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/missing-precedents": { parameters: { query?: never; @@ -3556,6 +3690,22 @@ export interface paths { export type webhooks = Record; export interface components { schemas: { + /** AdapterMigrationRequest */ + AdapterMigrationRequest: { + /** Action */ + action: string; + /** Agent */ + agent?: string | null; + /** To */ + to?: string | null; + /** Model */ + model?: string | null; + /** + * Relax Tools + * @default false + */ + relax_tools: boolean; + }; /** AgentCommentRequest */ AgentCommentRequest: { /** Body */ @@ -4428,6 +4578,90 @@ export interface components { */ appeal_type: string; }; + /** PlanEditRequest */ + PlanEditRequest: { + /** Plan Number */ + plan_number: string; + /** + * Display Name + * @default + */ + display_name: string; + /** + * Plan Type + * @default + */ + plan_type: string; + /** + * Gazette Date + * @default + */ + gazette_date: string; + /** + * Yalkut Number + * @default + */ + yalkut_number: string; + /** + * Purpose + * @default + */ + purpose: string; + /** Aliases */ + aliases?: string[] | null; + }; + /** PlanMergeRequest */ + PlanMergeRequest: { + /** Source Id */ + source_id: string; + /** Target Id */ + target_id: string; + }; + /** PlanReviewRequest */ + PlanReviewRequest: { + /** Review Status */ + review_status: string; + }; + /** PlanUpsertRequest */ + PlanUpsertRequest: { + /** Plan Number */ + plan_number: string; + /** + * Display Name + * @default + */ + display_name: string; + /** + * Plan Type + * @default + */ + plan_type: string; + /** + * Gazette Date + * @default + */ + gazette_date: string; + /** + * Yalkut Number + * @default + */ + yalkut_number: string; + /** + * Purpose + * @default + */ + purpose: string; + /** + * Review Status + * @default approved + */ + review_status: string; + /** + * Aliases + * @default [] + */ + aliases: string[]; + }; /** PrecedentCreateRequest */ PrecedentCreateRequest: { /** Quote */ @@ -9354,6 +9588,39 @@ export interface operations { }; }; }; + operations_agent_migrate_adapter_api_operations_agents_migrate_adapter_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdapterMigrationRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; digest_get_api_digests__digest_id__get: { parameters: { query?: never; @@ -10016,6 +10283,237 @@ export interface operations { }; }; }; + plans_list_api_plans_get: { + parameters: { + query?: { + review_status?: string; + q?: string; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + plan_create_api_plans_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PlanUpsertRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + plan_duplicates_api_plans__plan_id__duplicates_get: { + parameters: { + query?: never; + header?: never; + path: { + plan_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + plan_get_api_plans__plan_id__get: { + parameters: { + query?: never; + header?: never; + path: { + plan_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + plan_edit_api_plans__plan_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + plan_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PlanEditRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + plan_review_api_plans__plan_id__review_post: { + parameters: { + query?: never; + header?: never; + path: { + plan_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PlanReviewRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + plan_merge_api_plans_merge_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PlanMergeRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; missing_precedents_list_api_missing_precedents_get: { parameters: { query?: { diff --git a/web/app.py b/web/app.py index e95d879..fd1b30c 100644 --- a/web/app.py +++ b/web/app.py @@ -5786,7 +5786,7 @@ async def api_chair_pending(): "key": "plans", "label": "תכניות הממתינות לאישור", "description": "תכניות שחולצו אוטומטית מהחלטות — מצוטטות בבלוק ט רק לאחר אישורך.", "count": pl_count, "severity": "medium" if pl_count else "ok", - "href": "/precedents", "oldest_at": pl_oldest.isoformat() if pl_oldest else None, + "href": "/precedents?tab=plans", "oldest_at": pl_oldest.isoformat() if pl_oldest else None, "sample": [{"text": r["name"], "source": (r["purpose"] or "")[:80]} for r in pl_sample], })