Skip to content

feat(s-orgtz-unify): unify the org timezone onto the working calendar (R56)#294

Merged
CoJoA13 merged 18 commits into
mainfrom
feat/s-orgtz-unify
Jun 26, 2026
Merged

feat(s-orgtz-unify): unify the org timezone onto the working calendar (R56)#294
CoJoA13 merged 18 commits into
mainfrom
feat/s-orgtz-unify

Conversation

@CoJoA13

@CoJoA13 CoJoA13 commented Jun 26, 2026

Copy link
Copy Markdown
Owner

S-orgtz-unify — unify the org timezone onto the working calendar (R56)

Collapses the three drifting org-timezone sources (env easysynq_org_timezone, organization.timezone, working_calendar.timezone) into one canonical, DB-resolved tz so review dates, the review_state badge, snap/timer, and notification rendering all judge dates in one frame. Closes the named cross-cutting residual + R29's D-3 divergence.

What

  • services/common/org_clock.py (new): resolve_org_tz (chain working_calendar.timezone → organization.timezone → env → UTC, fail-safe) + pick_tz + a request/sweep-scoped ContextVar (current_org_tz / set_request_org_tz [no reset, per-request-task isolation] / using_org_tz [reset CM]). resolve_working_calendar sources its tz from the same pick_tzparity by construction.
  • Hybrid mechanism (R56/D-2): today_org() is contextvar-backed — set at the auth boundary (get_current_user) and wrapped around every worker render; the compute functions (compute_next_review_due, read_cadence, _last_released_effective_from) take an explicit org_tz.
  • R8 cutover stays UTC — only date display/derivation moves to the canonical tz.
  • Folded in: _fmt_date re-converts aware datetimes to the org tz; OVERDUE is now now_is_working-gated (closes R55 D-5's weekend-pierce exemption); cli/backfill_review_dates.py recomputes stored next_review_due into the canonical tz (idempotent, --dry-run; MR next-due is derived → no backfill).

Scope

BE + CLI + docs only. No migration (head stays 0067) · no new permission key · no web/FE change · env var kept as the bottom fallback · organization.timezone & working_calendar.timezone stay independently editable (no S-notify-7 regression). New register decision R56 + amend-notes on R8 & R55.

Review

Built subagent-driven (10 tasks, a fresh implementer + task-reviewer each). A final whole-branch review (opus + diff-critic) caught one east-of-UTC-only false-PASS (masked on the dev Chicago org + CI's UTC): three worker sweeps (sweep_reviews, sweep_mgmt_reviews, ack sweep) rendered the "Due by" date with the contextvar unset → _fmt_date reframed the cal.tz due_at to the previous UTC day (regressing the Codex #291 fix), and _cutover stored next_review_due in UTC via the release_due Beat — fixed by wrapping each worker enqueue in using_org_tz(cal.tz) and resolving the tz explicitly in _cutover. The full -m unit gate then caught a pre-existing _fmt_date test needing the new render context — also fixed.

Tests

Local: ruff + mypy src (412 files) + -m unit 1062 passed green. Integration + migrations are CI-gated (no local Docker). Deltas: +12 unit / +5 integration, web unchanged (1292, no FE).

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

CoJoA13 and others added 18 commits June 26, 2026 00:51
…g calendar

Canonical org tz resolved cal.tz -> org.tz -> env -> UTC (D-1); hybrid
contextvar + explicit mechanism (D-2); folds in _fmt_date render hardening,
the OVERDUE now_is_working gate, and a next_review_due backfill CLI (D-3);
R8 cutover stays UTC (D-4). Closes the named org-tz cross-cutting residual
and R55 D-3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…contextvar leak)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…calendar tz via pick_tz (parity)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…en missing-row test (parity, not == DEFAULT_CALENDAR)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…boundary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ts the org-tz contextvar

Closes the review I1 gap: the HTTP propagation test is only mutation-distinguishing
~14/24h; this monkeypatched unit test of get_current_user is time-independent and
fully mutation-distinguishing (no Docker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tz; wrap the escalation render

render.py _fmt_date: when value is an aware datetime, astimezone(current_org_tz()).date() instead
of the raw UTC .date() — fixes the east-of-UTC date shown one day early for a UTC-stored due_at.
Naive datetimes and date objects are unchanged. Two new unit tests: aware→Tokyo reconvert is
mutation-distinguishing (UTC fallback shows 2026-06-28; Tokyo context shows 2026-06-29), naive/date
pass-through confirmed.

escalation.py process_task_timers: import using_org_tz alongside pick_tz; wrap the fired=0 + for
step in due_steps(…) block in with using_org_tz(calendar.tz): so that emit_task_event → render →
_fmt_date sees the calendar tz (which equals resolve_org_tz by pick_tz parity — no extra DB hit).
The await session.commit() and return fired remain outside the with. Mechanical reindent only.

digest.py: no change needed — bundle_user_digest renders only recipient.first_name / item_count /
items (pre-rendered title strings) / prefs_link; no {{ … | date }} slot for a due_at datetime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e canonical org tz

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…olved canonical tz

Replace every `_org_tz()` (env-tz contextvar, unset in tests → UTC) in the
two affected integration files with `await _canonical_tz()` — a thin async
helper that calls `resolve_default_org_tz(session)`, the same DB chain the
server now uses (cal.tz → org.timezone → env → UTC).

test_periodic_review.py (10 sites):
- module-level `_org_tz` dropped from the vault.review import
- two inline `from … import _org_tz` re-imports removed
- `_canonical_tz()` module-level helper added
- all 10 expectation sites (eff_from_dt.astimezone, datetime.now,
  di.last_reviewed_at.astimezone, backdated_eff_from.astimezone) rewritten

test_reports.py (1 site):
- `_canonical_tz()` helper added
- redundant inline `get_sessionmaker`/`_org_tz` imports removed
- `today_date` in test_checklist_overdue_review_leg rewritten (the "yesterday"
  written to next_review_due must be yesterday in the server's tz so the
  overdue_review flag flips correctly under a divergent calendar.timezone)

In CI (org calendar tz == UTC == env UTC) behaviour is identical.
The rewrite removes fragility against test_org_clock.py temporarily mutating
the calendar tz to non-UTC — if a restore is skipped/reordered an env-UTC
expectation would mismatch the canonical-non-UTC server.

Mutation-verify (step 4): deferred to CI/live-smoke (requires Docker).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the canonical tz (final-review)

Three worker sweeps (sweep_reviews, sweep_mgmt_reviews, sweep_acks) enqueued
task.assigned notifications with the org-tz contextvar unset, so _fmt_date
fell back to UTC for a cal.tz-aware due_at — rendering the UTC-previous-day
date for east-of-UTC orgs (regression of the Codex #291 _fmt_date fix).
_cutover similarly called current_org_tz() (contextvar) instead of an
explicit resolve, so the Beat release path stored next_review_due in the
wrong tz frame.

Fixes:
- services/vault/review.py sweep_reviews: with using_org_tz(cal.tz) around enqueue
- services/mgmt_review/cadence.py sweep_mgmt_reviews: with using_org_tz(cal.tz) around enqueue
- services/ack/sweep.py sweep_acks: resolve_org_tz once per doc + using_org_tz(org_tz) around enqueue
- services/vault/lifecycle.py _cutover: replace current_org_tz() with await resolve_org_tz(session, doc.org_id)

Adds a CI-authoritative integration test: sweep_reviews under Asia/Tokyo calendar
asserts the notification body contains the Tokyo-local Monday date, not the
UTC-previous-day (Sunday) — mutation-distinguishing against the missing wrap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…snap test

The §3.4 _fmt_date change re-converts via current_org_tz(); the materialize/sweep
render now runs inside using_org_tz(cal.tz), so this pre-existing test must set the
context. Caught by the full -m unit suite (the targeted per-task suites missed this
cross-file _fmt_date caller in test_duedate_snap.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fication test

CI integration run PROVED the production fix works (body rendered the Tokyo date
2026-06-29, not the UTC Sunday). The test's (7 - isoweekday()) %7 computed the next
SUNDAY, set next_review_due there, and the sweep correctly SNAPPED due_at forward to
Monday — so the asserted (unsnapped) Sunday mismatched. Use (1 - isoweekday()) %7 to
target a real Monday (a Tokyo working day, no snap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CoJoA13 CoJoA13 merged commit 0008b08 into main Jun 26, 2026
11 checks passed
@CoJoA13 CoJoA13 deleted the feat/s-orgtz-unify branch June 26, 2026 11:01
CoJoA13 added a commit that referenced this pull request Jun 26, 2026
Slice-history narrative + CLAUDE.md learning (drop oldest S-notify-3b for the ~8 cap)
+ Current-status pointer for the org-timezone unification (R56). No code change.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant