fix(tz): timezone-aware datetimes — unblock TZ=Asia/Jerusalem (#210)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

Backend follow-up to INV-UI9 (X6 §"אודיט-נלווה"): make server-side
datetimes explicit so flipping the container to TZ=Asia/Jerusalem cannot
silently skew stored or displayed timestamps. Storage stays UTC; human
display is Israel time.

- audit.py: naive datetime.utcnow() → datetime.now(timezone.utc) for the
  TIMESTAMPTZ audit_log.created_at. Under TZ=Asia/Jerusalem the naive UTC
  wall-clock would be read as Israel-local and stored ~3h off. (§6: no
  silent skew.)
- calibrate_effort.py: _ts() report stamp now Israel-time via
  ZoneInfo("Asia/Jerusalem") with a filename-safe "-IL" suffix (script runs
  on the UTC host); added a labeled "שעון ישראל" line to the report header
  so the chair-facing timestamp is unambiguous. --self-test still ALL PASS.
- research_md.py: display fields made deterministic Israel-tz —
  update_chair_position timestamp (datetime.now(IL_TZ)) and parse() mtime
  (datetime.fromtimestamp(..., tz=IL_TZ)) — correct regardless of process TZ.

Left intentionally Israel-local-day date.today()/year-cutoff call sites
untouched (drafting/party_claims_summary/block_writer/court_fetch_supreme)
— a Jerusalem committee wants the Israel calendar day; they resolve
correctly once TZ flips.

Invariants: INV-UI9 (store-UTC / display-Israel, server side); §6
(timezone-aware datetimes, no silent ~3h skew).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 17:24:17 +00:00
parent d3d99180ec
commit 9402c7302f
3 changed files with 24 additions and 6 deletions

View File

@@ -15,8 +15,14 @@ import os
import re
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
from typing import Any
# Display timezone (INV-UI9): timestamps surfaced to humans are rendered in
# Israel time deterministically, regardless of the process clock (the container
# may run UTC or Asia/Jerusalem). Storage stays UTC; this is display-only.
IL_TZ = ZoneInfo("Asia/Jerusalem")
# Placeholder strings — any of these means "not yet filled"
CHAIR_POSITION_PLACEHOLDERS = (
"[ימולא ע\"י יו\"ר הוועדה]",
@@ -191,7 +197,7 @@ def parse(file_path: Path) -> dict[str, Any]:
date_str = date_match.group(1) if date_match else ""
stat = file_path.stat()
mtime_iso = datetime.fromtimestamp(stat.st_mtime).isoformat()
mtime_iso = datetime.fromtimestamp(stat.st_mtime, tz=IL_TZ).isoformat()
result: dict[str, Any] = {
"header": {
@@ -354,7 +360,7 @@ def update_chair_position(
"saved": True,
"section_id": section_id,
"preview": preview,
"timestamp": datetime.now().isoformat(),
"timestamp": datetime.now(IL_TZ).isoformat(),
}