fix(web-ui): תצוגת זמנים אנושית דטרמיניסטית ב-Asia/Jerusalem (אחסון נשאר UTC) — INV-UI9 (#210) #367

Merged
chaim merged 1 commits from worktree-agent-a441570a4556d14e5 into main 2026-06-30 17:20:35 +00:00
Owner

מה ולמה

כל חותמת-זמן אנושית ב-web-ui מוצגת כעת דטרמיניסטית ב-Asia/Jerusalem. האחסון נשאר UTC (Postgres TIMESTAMPTZ, ה-API מחזיר ISO-UTC) — תוקנה שכבת-התצוגה בלבד. עד כה הפירמוט היה ad-hoc (toLocaleDateString("he-IL"), date-fns-less new Date(...).get*(), toISOString().slice(0,10)), שתלוי ב-tz של ה-runtime → מציג UTC תחת SSR (הקונטיינר ב-UTC) או בכל runtime לא-ישראלי. נעיצת Asia/Jerusalem (Intl) מבטיחה ש-SSR והדפדפן מסכימים.

אין שינוי layout/עיצוב/IA — רק ה-tz. צורת התאריך/שעה הנראית נשמרה זהה. ⇒ שער Claude Design לא חל (תיקון-נכונות-תצוגה, לא שינוי-UI).

TaskMaster legal-ai #210.

מה נעשה

  • מפרמט-תאריך יחיד חדש web-ui/src/lib/format-date.ts (G2): formatDate (dd/MM/yyyy) · formatDateShort · formatDateLong (month long) · formatDateTime · formatDateTimeFull · formatTime (HH:mm) · formatIsoDate (YYYY-MM-DD) · formatRelative ("לפני…" ב-3 וריאנטי-ניסוח: long/short/tight) · עוזרי-Israel getIsraelYear / israelDayKey / israelMidnightMs / todayIsraelMidnightMs / israelParts. כולם נעוצים timeZone: "Asia/Jerusalem", locale he-IL.
  • נותבו 30 קבצים עם פירמוט-זמן ad-hoc דרך המפרמט. פירמוט-מספרים (n.toLocaleString) הושאר ללא נגיעה (אינו חותמת-זמן).
  • חריג מוצהר: קלט <input type="datetime-local"> (בקרת-burst ב-/operations, toLocalInput/nextSaturday18) הוא ערך ניתן-לכתיבה ע"י המפעיל, לא תצוגת-מאוחסן — נשאר בנייה מקומית (מחוץ לתחום "שכבת-תצוגה בלבד").
  • ספ: נוסף INV-UI9 ל-docs/spec/X6-ui-api-contract.md — אחסון UTC, תצוגה ב-Asia/Jerusalem; ומקור-אמת-יחיד למפרמט-תאריך בכללי-העיצוב.

אודיט-נלווה — TZ של הקונטיינר (לא יושם; לבדיקת המפעיל)

האם הגדרת TZ=Asia/Jerusalem בקונטיינר בטוחה? כמעט-לחלוטין — עם תיקון-קדם אחד:

  • ⚠️ mcp-server/src/legal_mcp/services/audit.py:41datetime.utcnow() — naive, נכתב ל-TIMESTAMPTZ בלי session-tz נעוץ → תחת flip ל-Asia/Jerusalem הערך (wall-clock=UTC) ייקרא כ-Israel-local ויאוחסן 3 שעות שגוי. לתקן ל-datetime.now(timezone.utc) לפני flip.
  • date.today() (drafting / party_claims_summary / block_writer — חותמת-תאריך מודפסת על ההחלטה; court_fetch_supreme.py:91 — שנת-cutoff): flip מזיז יום רק בגבול-חצות, ו-Israel-local הוא דווקא הרצוי לוועדת-ירושלים → לא מסכן.
  • datetime.now().isoformat() (research_md.py:357) ו-datetime.fromtimestamp(st_mtime) (research_md.py:194): שדה-תשובה/תצוגה בלבד, לא נשמר ולא מושווה → קוסמטי.
  • כל שאר ה-now ב-web/ כבר tz-aware (datetime.now(timezone.utc) / ZoneInfo("Asia/Jerusalem")).

Invariants — הצהרה

  • נוגע / מקיים: G2 (מקור-אמת יחיד — מפרמט-תאריך אחד, אין פירמוט-זמן מקביל-מפוזר); INV-UI9 חדש (אחסון UTC / תצוגה Asia/Jerusalem). שכבת-תצוגה בלבד — אין מגע ב-API/אחסון, אין מסלול מקביל (G2), אין בליעת-שגיאה.

צ'קליסט — פרוטוקול כתיבת-קוד

  • קראתי docs/spec/00-constitution.md (G2) + X6-ui-api-contract.md + web-ui/AGENTS.md לפני הכתיבה
  • השינוי לא יוצר מסלול מקביל (G2) — איחד פירמוט מפוזר למפרמט יחיד; לא מתקן תסמין בקריאה (G1) — האחסון כבר UTC, התיקון בתצוגה
  • לא הוספתי מגע-Paperclip מחוץ ל-Port (G12)
  • אין בליעה שקטה — fallback מפורש (/"") על ערך חסר/לא-תקין, כפי שהיה
  • בדקתי מול gap-audit.md — לא נגעתי ב-FU/GAP ממופה
  • בדיקות: tsc --noEmit נקי; eslint על הקבצים — רק warnings קיימים-מראש (TanStack Table memoization)

אימות

  • npx tsc --noEmitנקי (0 שגיאות).
  • בדיקת-דטרמיניזם: הרצת המפרמטים על instant 2026-06-30T22:30:00Z תחת TZ=UTC ותחת TZ=America/New_York — פלט זהה ושל Israel: formatTime → 01:30, formatDate → 01.07.2026, formatIsoDate → 2026-07-01 (ולא 22:30/30.06 של UTC). israelMidnightMs ממפה את כל הרגעים של יום-ישראלי לאותו midnight-ms, וימים עוקבים נבדלים ב-86400000ms בדיוק (השוואות-הדיון של cases-table נשמרות).

🤖 Generated with Claude Code

## מה ולמה כל חותמת-זמן אנושית ב-web-ui מוצגת כעת **דטרמיניסטית ב-Asia/Jerusalem**. **האחסון נשאר UTC** (Postgres `TIMESTAMPTZ`, ה-API מחזיר ISO-UTC) — תוקנה **שכבת-התצוגה בלבד**. עד כה הפירמוט היה ad-hoc (`toLocaleDateString("he-IL")`, `date-fns`-less `new Date(...).get*()`, `toISOString().slice(0,10)`), שתלוי ב-tz של ה-runtime → מציג **UTC תחת SSR** (הקונטיינר ב-UTC) או בכל runtime לא-ישראלי. נעיצת `Asia/Jerusalem` (Intl) מבטיחה ש-SSR והדפדפן מסכימים. **אין שינוי layout/עיצוב/IA — רק ה-tz.** צורת התאריך/שעה הנראית נשמרה זהה. ⇒ **שער Claude Design לא חל** (תיקון-נכונות-תצוגה, לא שינוי-UI). TaskMaster `legal-ai` #210. ### מה נעשה - **מפרמט-תאריך יחיד חדש** `web-ui/src/lib/format-date.ts` (G2): `formatDate` (dd/MM/yyyy) · `formatDateShort` · `formatDateLong` (month long) · `formatDateTime` · `formatDateTimeFull` · `formatTime` (HH:mm) · `formatIsoDate` (YYYY-MM-DD) · `formatRelative` ("לפני…" ב-3 וריאנטי-ניסוח: long/short/tight) · עוזרי-Israel `getIsraelYear` / `israelDayKey` / `israelMidnightMs` / `todayIsraelMidnightMs` / `israelParts`. כולם נעוצים `timeZone: "Asia/Jerusalem"`, locale `he-IL`. - **נותבו 30 קבצים** עם פירמוט-זמן ad-hoc דרך המפרמט. פירמוט-**מספרים** (`n.toLocaleString`) הושאר ללא נגיעה (אינו חותמת-זמן). - **חריג מוצהר:** קלט `<input type="datetime-local">` (בקרת-burst ב-`/operations`, `toLocalInput`/`nextSaturday18`) הוא ערך **ניתן-לכתיבה** ע"י המפעיל, לא תצוגת-מאוחסן — נשאר בנייה מקומית (מחוץ לתחום "שכבת-תצוגה בלבד"). - **ספ:** נוסף **INV-UI9** ל-[`docs/spec/X6-ui-api-contract.md`](docs/spec/X6-ui-api-contract.md) — אחסון UTC, תצוגה ב-Asia/Jerusalem; ומקור-אמת-יחיד למפרמט-תאריך בכללי-העיצוב. ### אודיט-נלווה — TZ של הקונטיינר (לא יושם; לבדיקת המפעיל) האם הגדרת `TZ=Asia/Jerusalem` בקונטיינר בטוחה? **כמעט-לחלוטין — עם תיקון-קדם אחד:** - ⚠️ **`mcp-server/src/legal_mcp/services/audit.py:41` — `datetime.utcnow()`** — naive, נכתב ל-`TIMESTAMPTZ` בלי session-tz נעוץ → תחת flip ל-Asia/Jerusalem הערך (wall-clock=UTC) ייקרא כ-Israel-local ויאוחסן **3 שעות שגוי**. **לתקן ל-`datetime.now(timezone.utc)` לפני flip.** - ✅ `date.today()` (drafting / party_claims_summary / block_writer — חותמת-תאריך מודפסת על ההחלטה; `court_fetch_supreme.py:91` — שנת-cutoff): flip מזיז יום רק בגבול-חצות, ו-Israel-local הוא דווקא הרצוי לוועדת-ירושלים → **לא מסכן**. - ✅ `datetime.now().isoformat()` (research_md.py:357) ו-`datetime.fromtimestamp(st_mtime)` (research_md.py:194): שדה-תשובה/תצוגה בלבד, לא נשמר ולא מושווה → קוסמטי. - כל שאר ה-now ב-`web/` כבר tz-aware (`datetime.now(timezone.utc)` / `ZoneInfo("Asia/Jerusalem")`). ## Invariants — הצהרה - **נוגע / מקיים:** **G2** (מקור-אמת יחיד — מפרמט-תאריך **אחד**, אין פירמוט-זמן מקביל-מפוזר); **INV-UI9** חדש (אחסון UTC / תצוגה Asia/Jerusalem). שכבת-תצוגה בלבד — אין מגע ב-API/אחסון, אין מסלול מקביל (G2), אין בליעת-שגיאה. ## צ'קליסט — פרוטוקול כתיבת-קוד - [x] קראתי `docs/spec/00-constitution.md` (G2) + `X6-ui-api-contract.md` + `web-ui/AGENTS.md` לפני הכתיבה - [x] השינוי **לא** יוצר מסלול מקביל (G2) — איחד פירמוט מפוזר למפרמט יחיד; לא מתקן תסמין בקריאה (G1) — האחסון כבר UTC, התיקון בתצוגה - [x] **לא** הוספתי מגע-Paperclip מחוץ ל-Port (G12) - [x] אין בליעה שקטה — fallback מפורש (`—`/`""`) על ערך חסר/לא-תקין, כפי שהיה - [x] בדקתי מול `gap-audit.md` — לא נגעתי ב-FU/GAP ממופה - [x] בדיקות: `tsc --noEmit` נקי; eslint על הקבצים — רק warnings קיימים-מראש (TanStack Table memoization) ## אימות - `npx tsc --noEmit` — **נקי** (0 שגיאות). - **בדיקת-דטרמיניזם:** הרצת המפרמטים על instant `2026-06-30T22:30:00Z` תחת `TZ=UTC` ותחת `TZ=America/New_York` — פלט **זהה** ושל Israel: `formatTime → 01:30`, `formatDate → 01.07.2026`, `formatIsoDate → 2026-07-01` (ולא 22:30/30.06 של UTC). `israelMidnightMs` ממפה את כל הרגעים של יום-ישראלי לאותו midnight-ms, וימים עוקבים נבדלים ב-86400000ms בדיוק (השוואות-הדיון של `cases-table` נשמרות). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chaim added 1 commit 2026-06-30 17:19:21 +00:00
fix(web-ui): display all human-facing timestamps in Asia/Jerusalem (deterministic)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
67c2c43777
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>
chaim merged commit d3d99180ec into main 2026-06-30 17:20:35 +00:00
chaim deleted branch worktree-agent-a441570a4556d14e5 2026-06-30 17:20:35 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ezer-mishpati/legal-ai#367