Merge pull request 'fix(tz): timezone-aware datetimes — unblock TZ=Asia/Jerusalem (#210)' (#368) from worktree-agent-a29624b2a56f70804 into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
G12 Leak-Guard / leak-guard (push) Successful in 5s
Lint — undefined names / undefined-names (push) Successful in 9s

This commit was merged in pull request #368.
This commit is contained in:
2026-06-30 17:25:55 +00:00
3 changed files with 24 additions and 6 deletions

View File

@@ -7,7 +7,7 @@ from __future__ import annotations
import json import json
import logging import logging
from datetime import datetime from datetime import datetime, timezone
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from legal_mcp.services import db from legal_mcp.services import db
@@ -38,7 +38,7 @@ async def log_action(
VALUES ($1, $2, $3, $4, $5, $6, $7)""", VALUES ($1, $2, $3, $4, $5, $6, $7)""",
uuid4(), action, case_id, document_id, uuid4(), action, case_id, document_id,
json.dumps(details or {}, ensure_ascii=False, default=str), json.dumps(details or {}, ensure_ascii=False, default=str),
user, datetime.utcnow(), user, datetime.now(timezone.utc),
) )
logger.info("AUDIT: %s | case=%s | user=%s | %s", action, case_id, user, logger.info("AUDIT: %s | case=%s | user=%s | %s", action, case_id, user,
json.dumps(details or {}, ensure_ascii=False)[:200]) json.dumps(details or {}, ensure_ascii=False)[:200])

View File

@@ -15,8 +15,14 @@ import os
import re import re
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from zoneinfo import ZoneInfo
from typing import Any 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" # Placeholder strings — any of these means "not yet filled"
CHAIR_POSITION_PLACEHOLDERS = ( 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 "" date_str = date_match.group(1) if date_match else ""
stat = file_path.stat() 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] = { result: dict[str, Any] = {
"header": { "header": {
@@ -354,7 +360,7 @@ def update_chair_position(
"saved": True, "saved": True,
"section_id": section_id, "section_id": section_id,
"preview": preview, "preview": preview,
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now(IL_TZ).isoformat(),
} }

View File

@@ -58,8 +58,9 @@ import json
import logging import logging
import os import os
import sys import sys
from datetime import datetime, timezone from datetime import datetime
from pathlib import Path from pathlib import Path
from zoneinfo import ZoneInfo
from statistics import mean from statistics import mean
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -344,8 +345,17 @@ async def _run(args, ts: str) -> dict:
return {"dry_run": False, "grid": grid_summary, "by_block": by_block} return {"dry_run": False, "grid": grid_summary, "by_block": by_block}
IL_TZ = ZoneInfo("Asia/Jerusalem")
def _ts() -> str: def _ts() -> str:
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") """Filename-safe report stamp in Israel time (INV-UI9: human-facing display).
This script runs on the host, which is UTC, so a process-clock stamp would
mislead the chair. We pin Asia/Jerusalem and suffix ``IL`` (instead of a bare
``Z``) so the displayed/filename time is unambiguously Israel-local.
"""
return datetime.now(IL_TZ).strftime("%Y%m%dT%H%M%S-IL")
def _write_report(result: dict, ts: str) -> tuple[Path, Path]: def _write_report(result: dict, ts: str) -> tuple[Path, Path]:
@@ -356,8 +366,10 @@ def _write_report(result: dict, ts: str) -> tuple[Path, Path]:
g = result["grid"] g = result["grid"]
n = g["n_finals"] n = g["n_finals"]
il_now = datetime.now(IL_TZ).strftime("%Y-%m-%d %H:%M")
lines = [ lines = [
f"# #208 — כיול model×effort מול הסופיים — {ts}\n", f"# #208 — כיול model×effort מול הסופיים — {ts}\n",
f"> נוצר: {il_now} (שעון ישראל · Asia/Jerusalem)\n",
f"> ⚠️ **גודל-מדגם: {n} סופיים** ({', '.join(g['finals']) or ''}). " f"> ⚠️ **גודל-מדגם: {n} סופיים** ({', '.join(g['finals']) or ''}). "
"זוהי **עדות-כיוון, לא רגרסיה** — מעט תיקים בעלי סופי-עלוי. " "זוהי **עדות-כיוון, לא רגרסיה** — מעט תיקים בעלי סופי-עלוי. "
"ההמלצה אדוויזורית; ההכרעה בידי היו\"ר/המפעיל.\n", "ההמלצה אדוויזורית; ההכרעה בידי היו\"ר/המפעיל.\n",