Skip to content

fix(timezone): stop persisting poll/auto-resolved timezones to .env - #1800

Open
Zenetusken wants to merge 2 commits into
agent0ai:mainfrom
Zenetusken:fix/timezone-auto-persistence
Open

fix(timezone): stop persisting poll/auto-resolved timezones to .env#1800
Zenetusken wants to merge 2 commits into
agent0ai:mainfrom
Zenetusken:fix/timezone-auto-persistence

Conversation

@Zenetusken

@Zenetusken Zenetusken commented Aug 2, 2026

Copy link
Copy Markdown

What this PR does

Browser- and auto-resolved timezones are now runtime-only and are never written back to .env, so they can no longer clobber the user's saved DEFAULT_USER_TIMEZONE / DEFAULT_USER_UTC_OFFSET_MINUTES.

Behavioral change (intentional, please read)

In auto mode the resolved browser timezone no longer survives restart: after a restart the runtime timezone reverts to the .env value (explicit choice, or TZ/UTC fallback) until the first browser poll arrives. This is deliberate — persisting the last browser value is exactly the bug: any poll or scheduler call from a browser in a different timezone silently overwrote the user's saved default. Explicit (non-auto) timezone choices still persist exactly as before.

This also fixes a real crash: on a read-only /a0/usr mount, any /poll carrying a timezone raised OSError: [Errno 30] Read-only file system: '/a0/usr/.env' out of set_timezone → save_dotenv_value (uncaught).

Changes

  • helpers/localization.py: set_timezone() gains persist: bool = True; the two save_dotenv_value() calls are skipped when persist=False. Default preserves behavior for all existing callers.
  • helpers/settings.py: _apply_timezone_setting passes persist=_settings["timezone"] != TIMEZONE_AUTO — auto-resolved values are runtime-only; explicit choices persist.
  • helpers/state_snapshot.py: the poll/state-push path passes persist=False.
  • All six scheduler endpoints (scheduler_task_create/delete/run/update/tasks_list/tick) pass persist=False — the webui scheduler store sends getUserTimezone(), which in auto mode is the browser timezone, so these were the same clobbering vector as poll.
  • helpers/localization.py.dox.md / helpers/state_snapshot.py.dox.md updated for the new signature and semantics.

Tests

  • New/extended coverage in tests/test_timezone_regressions.py: poll path calls set_timezone(..., persist=False); explicit settings provably still persist (DEFAULT_USER_TIMEZONE written); parametrized behavioral test asserting all six scheduler endpoints call set_timezone with persist=False.
  • Verified against base: the new tests fail on the parent commit for the right reasons; full-suite failure set is byte-identical between base and head (only pre-existing environmental failures).

Known limitation (pre-existing, not made worse)

Runtime clobbering is only guarded on the persistence side: state_snapshot.py still applies a per-request poll timezone to the process-global Localization (including os.environ["TZ"] + tzset()), so two browsers in different timezones can flap the runtime timezone. Fixing that requires per-request rendering context rather than global state and is left for a follow-up.

The /poll snapshot builder applied every request's reported timezone
via localization.set_timezone(), which persisted it into usr/.env as
DEFAULT_USER_TIMEZONE. Any client reporting a wrong or spoofed
timezone (e.g. Firefox with resistFingerprinting, which reports UTC)
silently overwrote the user's saved default - no settings change
required, a read poll was enough.

The same hole existed in settings AUTO mode: the auto-resolved value
was persisted over the user's explicit default.

- helpers/localization.py: set_timezone() gains persist=True flag
- helpers/state_snapshot.py: poll path calls set_timezone(persist=False)
- helpers/settings.py: AUTO resolution passes persist=False; explicit
  fixed timezone choices still persist as before
- tests/test_timezone_regressions.py: regression test asserting AUTO
  mode applies the browser timezone at runtime without persisting it
The six scheduler API endpoints applied the client-supplied timezone via
localization.set_timezone() with the default persist=True, writing the
browser-reported value into usr/.env as DEFAULT_USER_TIMEZONE. The WebUI
scheduler store sends getUserTimezone(), which in AUTO mode is the browser
timezone - so any scheduler create/update/run/delete/list/tick call could
clobber the user's saved default, the same hole the poll path had.

- api/scheduler_task_{create,delete,run,update}.py, scheduler_tasks_list.py,
  scheduler_tick.py: pass persist=False; browser timezones are runtime-only
- tests/test_timezone_regressions.py: behavioral test invoking all six
  endpoint handlers asserting set_timezone is called with persist=False;
  poll/snapshot path test asserting persist=False and no .env writes;
  positive assertions that explicit fixed timezone settings still persist
- helpers/localization.py.dox.md, helpers/state_snapshot.py.dox.md: document
  the persist contract
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