fix(timezone): stop persisting poll/auto-resolved timezones to .env - #1800
Open
Zenetusken wants to merge 2 commits into
Open
fix(timezone): stop persisting poll/auto-resolved timezones to .env#1800Zenetusken wants to merge 2 commits into
Zenetusken wants to merge 2 commits into
Conversation
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
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 savedDEFAULT_USER_TIMEZONE/DEFAULT_USER_UTC_OFFSET_MINUTES.Behavioral change (intentional, please read)
In
automode the resolved browser timezone no longer survives restart: after a restart the runtime timezone reverts to the.envvalue (explicit choice, orTZ/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/usrmount, any/pollcarrying a timezone raisedOSError: [Errno 30] Read-only file system: '/a0/usr/.env'out ofset_timezone → save_dotenv_value(uncaught).Changes
helpers/localization.py:set_timezone()gainspersist: bool = True; the twosave_dotenv_value()calls are skipped whenpersist=False. Default preserves behavior for all existing callers.helpers/settings.py:_apply_timezone_settingpassespersist=_settings["timezone"] != TIMEZONE_AUTO— auto-resolved values are runtime-only; explicit choices persist.helpers/state_snapshot.py: the poll/state-push path passespersist=False.scheduler_task_create/delete/run/update/tasks_list/tick) passpersist=False— the webui scheduler store sendsgetUserTimezone(), 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.mdupdated for the new signature and semantics.Tests
tests/test_timezone_regressions.py: poll path callsset_timezone(..., persist=False); explicit settings provably still persist (DEFAULT_USER_TIMEZONEwritten); parametrized behavioral test asserting all six scheduler endpoints callset_timezonewithpersist=False.Known limitation (pre-existing, not made worse)
Runtime clobbering is only guarded on the persistence side:
state_snapshot.pystill applies a per-request poll timezone to the process-globalLocalization(includingos.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.