Files
legal-ai/docs/spec/X6-ui-api-contract.md
Chaim 67c2c43777
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
fix(web-ui): display all human-facing timestamps in Asia/Jerusalem (deterministic)
Storage stays UTC (DB TIMESTAMPTZ, API ISO-UTC) — only the display layer is
localized, and now deterministically: every timestamp renders pinned to
Asia/Jerusalem via a single Intl-based formatter, so SSR (UTC container) and
the browser agree on any runtime. No layout/visible-format change — only the tz.

- New single date formatter web-ui/src/lib/format-date.ts (G2): formatDate /
  formatDateShort / formatDateLong / formatDateTime / formatDateTimeFull /
  formatTime / formatIsoDate + Israel helpers getIsraelYear / israelDayKey /
  israelMidnightMs / israelParts + formatRelative (long/short/tight wording).
- Routed all ad-hoc toLocaleDateString/toLocaleTimeString/toLocaleString +
  hand-rolled new Date(...).get*() / toISOString().slice(0,10) timestamp call
  sites (30 files) through it. Number toLocaleString left untouched.
- Spec: added INV-UI9 to docs/spec/X6 (UTC storage, Asia/Jerusalem display).

Display-only; no layout/design/IA change → Claude Design gate N/A.
Invariants: G2 (single date formatter, no parallel ad-hoc formatting), INV-UI9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:18:09 +00:00

16 KiB
Raw Blame History

X6 — חוזה UI↔API וכללי-עיצוב הממשק (UI↔API Contract & Design Rules)

קובץ-תחום זה כפוף ל-חוקת המערכת והוא ה-deep-dive על הממשק (web-ui) וחוזה ה-API בינו לבקאנד — שלא היה מכוסה בספ עד כה. הוא מגדיר: (א) חוזה-הקשר פרונט↔בק (OpenAPI כ-SSoT, מודלי-תשובה, envelope, SSE, טיפול-שגיאות); (ב) כללי-עיצוב הממשק — מקור-אמת יחיד ל-enums/תוויות, helpers משותפים, וחוזה-טופס לכל סוג-מסמך. הממצאים בפועל מתועדים ב-ui-audit.md.

שני סוגי invariant כאן. UI1UI5 הם הנדסיים (חוזה-API/קליינט כללי — ≥3 מקורות + סטטוס). UI6 (חוזה-טופס) הוא פרויקטלי-תפעולי, נגזר מ-X8, ומשרת G9.


1. ארכיטקטורה קיימת

  • web-ui — Next.js 16 + TS + Tailwind v4 + shadcn + TanStack Query. 13 דפים (ראה ui-audit.md).
  • Proxynext.config.ts: /api/*NEXT_PUBLIC_API_ORIGIN (ברירת-מחדל http://127.0.0.1:8000); /openapi.json → schema של ה-FastAPI.
  • לקוחclient.ts: apiRequest<T> + ApiError + makeQueryClient. 18 מודולי-API.
  • טיפוסיםtypes.ts (auto-gen openapi-typescript, 124 operations). npm run api:types.
  • SSEsse.ts: openSSE (progress של העלאות/עיבוד).
  • בקאנדweb/app.py: 143 endpoints, מונוליטי, ~60% ללא Pydantic response model.

2. Invariants של התחום

INV-UI1: ה-OpenAPI schema הוא ה-SSoT לחוזה — טיפוסי-לקוח נגזרים, לא ידניים-סוטים

כלל: חוזה ה-API מוגדר פעם אחת ב-OpenAPI (שמופק מהבקאנד); טיפוסי-ה-frontend נגזרים ממנו (openapi-typescript), ואינם מתוחזקים ידנית במקביל. אין "טיפוס-מראה" מקומי שמשכפל endpoint וסוטה ממנו. מופע של G2 (מקור-אמת יחיד). מקורות: OpenAPI Specification 3.1 (single contract / source of truth; JSON-Schema 2020-12) (https://spec.openapis.org/oas/latest.html) · Pact — consumer-driven contract testing (https://docs.pact.io/) · Speakeasy — Pact vs OpenAPI (provider-driven SSoT) (https://www.speakeasy.com/blog/pact-vs-openapi) | סטטוס: verified אכיפה: npm run api:types ב-CI; איסור טיפוסי-מראה ידניים. כיום אין — ה-frontend מתחזק טיפוסים ידניים. הפרה ידועה: cases.ts:1-9 מתעד מפורשות שה-/api/cases מחזיר unknown ולכן מוחזק טיפוס CaseDetail ידני; PracticeArea מוגדר ב-3 מקומות עם ערכים שונים (ui-audit.md, gap-audit GAP-30/31).

INV-UI2: לכל endpoint נצרך — response model מפורש (חוזה-שלמות API)

כלל: כל endpoint שה-UI צורך נושא response model מפורש (Pydantic), כך ש-OpenAPI מפיק טיפוס אמיתי (לא unknown/object). זהו פאֶט של G4 (שלמות-חוזה לפני צריכה). מקורות: OpenAPI 3.1 (schema objects) · Zalando RESTful API Guidelines (explicit schemas) (https://opensource.zalando.com/restful-api-guidelines/) · FastAPI Response Model docs (https://fastapi.tiangolo.com/tutorial/response-model/) | סטטוס: verified אכיפה: linter/CI שמסמן endpoint נצרך ללא response_model. כיום אין — ~60% מהendpoints ללא מודל. הפרה ידועה: רוב ה-endpoints ב-app.py מחזירים dict חופשי → unknown ב-types.ts (gap-audit GAP-30).

INV-UI3: envelope-תשובה ושגיאה עקבי על-פני ה-API

כלל: כל ה-endpoints חולקים מבנה-תשובה ומבנה-שגיאה אחיד (לא string-לפעמים-JSON-לפעמים). שגיאות לפי תבנית סטנדרטית (Problem Details). מופע של G2. מקורות: RFC 9457 — Problem Details for HTTP APIs (https://www.rfc-editor.org/rfc/rfc9457) · Zalando RESTful API Guidelines (consistent responses) · Microsoft REST API Guidelines (error structure) (https://github.com/microsoft/api-guidelines) | סטטוס: verified אכיפה: envelope משותף ב-app.py + handler-שגיאות גלובלי. כיום אין — מעורב string/JSON/{error}/{detail}. הפרה ידועה: search.py מחזיר "לא נמצאו תוצאות." או JSON; חלק מהכלים {error:...}, חלק raise (gap-audit GAP-32, X9 INV-TOOL1).

INV-UI4: אין בליעת-שגיאה ב-UI

כלל: כל מצב-שגיאה (fetch/mutation) מוצג או מטופל מפורשות — error boundary ו/או טיפול ב-error של useQuery/useMutation. אין כשל שקט שמשאיר את המשתמש בלי משוב. תואם כלל "אין בליעה שקטה" (חוקה §6). מקורות: React docs — Error Boundaries (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) · TanStack Query — Error handling (https://tanstack.com/query/latest/docs/framework/react/guides/query-functions#handling-and-throwing-errors) · Nielsen Norman Group — Error-Message Guidelines (https://www.nngroup.com/articles/error-message-guidelines/) | סטטוס: verified אכיפה: error boundary ברמת-האפליקציה + רכיב-שגיאה משותף; code-review. כיום חלקי — חלק מהדפים אינם מטפלים ב-error; כרטיסי-שגיאה משוכפלים ולא-עקביים. הפרה ידועה: ui-audit.md — כרטיס-שגיאה משוכפל ×3, fallback של SSE שמסתיר כישלון כ-"completed" (gap-audit GAP-32/33).

INV-UI5: חוזה-SSE/progress עם terminal states מוגדרים

כלל: ערוץ ה-progress (SSE) נושא terminal states מפורשים (completed/failed/timeout). אין הנחת-השלמה שקטה על timeout; אי-התאמות-TTL (frontend↔backend) נמנעות. נקשר ל-freshness (G6). מקורות: WHATWG HTML — Server-Sent Events / EventSource (https://html.spec.whatwg.org/multipage/server-sent-events.html) · MDN — Using server-sent events (https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) · TanStack Query — Important Defaults (staleTime/refetch) (https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults) | סטטוס: verified אכיפה: סכמת-אירוע SSE עם terminal state מפורש; יישור TTL. כיום: fallback של 10ש' מניח completed. הפרה ידועה: documents.ts:226-232 — timeout→{status:"completed"}; TTL 5ש' front מול 300ש' redis (gap-audit GAP-33).

INV-UI6: חוזה-טופס מוצהר לכל סוג-מסמך + שיקוף מקור-המילוי

כלל: לכל סוג-מסמך (מסמך-תיק / פסיקה חיצונית / החלטה פנימית) יש חוזה-טופס מוצהר — אילו שדות, חובה/רשות/אוטו/pending/editable — נגזר מ-X8; וה-UI משקף את מקור-המילוי (מסמן מה חולץ אוטומטית/ע"י-Opus מול מה שהיו"ר הזין), כדי שהיו"ר ידע מה לאמת. מופע של G9 (שקיפות-מקור). invariant פרויקטלי-תפעולי. מקור-סמכות: X8-field-provenance.md (טבלת-ה-provenance); feedback היו"ר. אכיפה: רכיב-טופס נגזר-X8 + אינדיקציית "מולא-ע"י-Opus"/"ממתין"/searchable. כיום אין — שדות-Opus מוצגים כשדות-עריכה רגילים ללא סימון. הפרה ידועה: precedents/[id]/page.tsxsummary/headnote/key_quote ללא חיווי-מקור; אין חיווי searchable (gap-audit GAP-36).

INV-UI7: aggregate-נגזר = מקור-אמת יחיד · כל mutation מבטל כל קורא (G2 בשכבת-ה-cache)

כלל: ל-aggregate/מונה נגזר-שרת (למשל /api/chair/pending) יש משטח-בעלים יחיד שמריץ את השאילתה; משטחים אחרים מצביעים אליו ולא מריצים מונה-מתחרה client-side. כל mutation שמשנה ערך הנקרא במשטח אחר חייב לבטל כל queryKey שקורא אותו — כולל aggregators חוצי-namespace — כך שאף משטח לא יציג ערך תקוע אחרי שינוי במשטח אחר. מקדד את X17 INV-IA1/IA2; מופע של G2 בשכבת-ה-TanStack-Query. מקור-סמכות: X17 (≥3 מקורות: TanStack Query invalidation · TkDodo "deriving client state" · NN/g consistency); ia-audit-redesign.md §D1. אכיפה: מונה-הגייטים חי רק ב-/approvals+['chair','pending']; /operations מצביע. כל mutation להלכות/ פסיקה-חסרה/הערות מבטל ['chair','pending'] (גל-1 #130, PR #207).

INV-UI8: שדה-response מרונדר-או-מוסר · חלקיות מוצגת

כלל: שדה שמופיע ב-response לרנדר או להסיר — אסור לזרוק אותו בשקט ב-frontend (אם אין צרכן — להסירו מה-response). KPI מוצג חייב להיות ממופה לצרכן אמיתי (לא דגל אינפורמטיבי-בלבד). aggregate מדויק כש-partial-failure השמיט תורם — להציג חלקיות ("+"/"חלקי"), לא מספר-מוקטן-כאילו-שלם. מקדד את X17 INV-IA5. מקור-סמכות: X17 (NN/g visibility-of-system-status · GOV.UK design-with-data); ia-audit-redesign.md §D3. אכיפה: halacha_backlog מרונדר ב-/operations (לא נזרק); findings_approved (review_status, צרכן אמיתי) החליף את findings_applied (דגל מת); מוני-סוכנים מסמנים "חלקי" כשחברה לא-נטענה (גל-1 #130).

INV-UI9: זמן-מאוחסן ב-UTC · תצוגה אנושית דטרמיניסטית ב-Asia/Jerusalem

כלל: האחסון נשאר UTC מקצה-לקצה — Postgres TIMESTAMPTZ, וה-API מחזיר ISO-UTC; אין נרמול-tz בשכבת-האחסון/ה-API. שכבת-התצוגה בלבד ממירה, ובאופן דטרמיניסטי: כל חותמת-זמן אנושית מרונדרת ב-timeZone: "Asia/Jerusalem" (locale he-IL), בלי תלות ב-tz של ה-host/קונטיינר/דפדפן. כיוון שהקונטיינר רץ ב-UTC, פירמוט ad-hoc (new Date(iso).toLocaleDateString("he-IL"), .getHours(), toISOString().slice(0,10)) מציג UTC תחת SSR (ושוב UTC בכל runtime לא-ישראלי) → drift. נעיצת Asia/Jerusalem מבטיחה ש-SSR והדפדפן מסכימים. פורמט-התצוגה (צורת התאריך/שעה) נשמר זהה — רק ה-tz מנורמל. מופע של G2 (מפרמט-תאריך יחיד, אין פירמוט-זמן מקביל-מפוזר) בשכבת-התצוגה. מקורות: ECMAScript Internationalization API — Intl.DateTimeFormat timeZone option (https://tc39.es/ecma402/#datetimeformat-objects) · MDN — Intl.DateTimeFormat (IANA time zones) (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) · Next.js — Rendering (SSR משתמש בשעון-השרת אם לא ננעץ) (https://nextjs.org/docs/app/building-your-application/rendering) | סטטוס: verified אכיפה: מפרמט-תאריך יחיד ב-web-ui/src/lib/format-date.ts (formatDate/formatDateShort/formatDateLong/formatDateTime/formatDateTimeFull/formatTime/ formatIsoDate/formatRelative + עוזרי-Israel getIsraelYear/israelDayKey/israelMidnightMs/ israelParts), כולם נעוצים ל-Asia/Jerusalem; כל משטחי-ה-UI מייבאים ממנו ולא מפרמטים tz ad-hoc. פירמוט-מספרים (n.toLocaleString) מחוץ-לתחום (אינו חותמת-זמן). חריג מוצהר: קלט <input type="datetime-local"> (בקרת-burst ב-/operations) הוא ערך הניתן-לכתיבה ע"י המפעיל, לא תצוגת- מאוחסן — נשאר בנייה מקומית. אודיט-נלווה (לא מיושם): הגדרת TZ=Asia/Jerusalem בקונטיינר בטוחה רק אם אף לוגיקה עסקית אינה תלויה בשעון-ה-process כ-UTC.


3. כללי-עיצוב (Design Rules) — נגזרים מה-invariants

  • SSoT ל-enums/תוויות/tones: כל enum (CaseStatus, PracticeArea, AppealSubtype, DocType, outcome) + תוויותיו + צבעיו מוגדרים פעם אחת ונצרכים מיבוא — לא משוכפלים בין דפים/רכיבים (מופע UI1/G2).
  • helpers משותפים: פירמוט-תאריך (מפרמט-יחיד נעוץ-Asia/Jerusalem ב-format-date.ts, INV-UI9), builder ל-FormData (העלאות), רכיב-שגיאה, query-config (intervals) — משותפים, לא מועתקים.
  • חוזי-טופס: ראה INV-UI6 (X8).

הממצאים הקונקרטיים (כפילויות, הגדרות-שגויות, redundancy) ב-ui-audit.md; התיקון — FU-10.


4. הפניות-אחיות