ML-198: document timezone plan
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
---
|
||||
id: doc-28
|
||||
title: "ML-198 Research: Store user timezone implementation routes"
|
||||
type: specification
|
||||
created_date: "2026-05-26 21:55"
|
||||
updated_date: "2026-05-26 22:02"
|
||||
tags:
|
||||
- research
|
||||
- timezone
|
||||
- liveview
|
||||
---
|
||||
|
||||
# ML-198 Research: Store user timezone implementation routes
|
||||
|
||||
## Research determination
|
||||
|
||||
This task requires research before planning because there is more than one viable persistence route and the chosen route affects multiple architectural layers: LiveView `on_mount`, application configuration, background workers, API/controller behaviour, database schema design, and tests.
|
||||
|
||||
## Current state
|
||||
|
||||
- The browser sends `Intl.DateTimeFormat().resolvedOptions().timeZone` in LiveSocket connect params from `assets/js/app.js`.
|
||||
- `MusicLibraryWeb.Hooks.GetTimezone` reads the connect param on each LiveView connection and assigns `:timezone`, falling back to `MusicLibrary.default_timezone/0`.
|
||||
- The fallback default timezone is configured as `config :music_library, default_timezone: "Europe/London"` and can be overridden by the `DEFAULT_TIMEZONE` runtime environment variable.
|
||||
- There is no persisted user/account/preferences schema. The app is effectively a single-user authenticated app using a login password, not per-user accounts.
|
||||
- Existing non-LiveView timezone-sensitive code still uses the default timezone or UTC:
|
||||
- `MusicLibrary.Worker.SendRecordsOnThisDayEmail` computes today via `MusicLibrary.default_timezone/0`.
|
||||
- `MusicLibraryWeb.MaintenanceLive.Index` manually sends the records-on-this-day email using `MusicLibrary.default_timezone/0`.
|
||||
- `MusicLibraryWeb.CollectionController.on_this_day/2` uses `Date.utc_today/0` when the API caller does not pass an explicit date.
|
||||
- `MusicLibrary.ListeningStats.cutoff_timestamp/2` has a fallback path intended to use the default timezone when no `:timezone` option is supplied.
|
||||
- `MusicLibrary.Secrets` already provides encrypted key-value storage for credentials such as the Last.fm session key, but timezone is not secret data.
|
||||
- Oban cron schedule timezone is currently hard-coded in config (`Europe/London`). Storing a timezone can update the date calculation at worker execution time, but it does not automatically make Oban's cron schedule run at 7 AM in the stored timezone.
|
||||
- Oban OSS `Oban.Plugins.Cron` loads its crontab statically at boot. Runtime, globally coordinated cron updates are an Oban Pro `DynamicCron` feature, so this project should not rely on mutating `Oban.Plugins.Cron` dynamically.
|
||||
|
||||
## Route 1 — Dedicated non-secret settings/preferences context (selected)
|
||||
|
||||
Create a small first-class persistence layer for non-secret application/user preferences. Since the app is single-user, this can be either:
|
||||
|
||||
- a `settings`/`app_settings` key-value table keyed by setting name, or
|
||||
- a `user_preferences` singleton table with a `timezone` column.
|
||||
|
||||
The context would expose timezone-specific functions such as:
|
||||
|
||||
- `current_timezone/0` — returns stored timezone first, then `MusicLibrary.default_timezone/0`.
|
||||
- `store_timezone/1` or `update_timezone_if_changed/1` — validates and persists browser-provided timezones.
|
||||
- Optional helper returning whether the value changed so the LiveView hook can decide whether to show the toast.
|
||||
|
||||
`GetTimezone` would only write when the browser provided a timezone connect param. First browser-provided value is stored silently; later differences update the store and notify with a translated info toast. LiveViews continue assigning `:timezone`, but from the resolved stored value after reconciliation.
|
||||
|
||||
Non-LiveView paths would call the shared resolver instead of reading the environment default directly.
|
||||
|
||||
### Pros
|
||||
|
||||
- Cleanly models timezone as non-secret application/user state rather than credential material.
|
||||
- Gives all code paths a single resolver that implements "stored first, environment fallback".
|
||||
- Easy to validate timezone values before writing or using them.
|
||||
- Keeps future non-secret preferences out of `Secrets`.
|
||||
- Works for LiveViews, controllers, workers, and tests.
|
||||
|
||||
### Cons
|
||||
|
||||
- Requires a migration, schema/context module, and tests.
|
||||
- Adds one small persisted table for a single setting unless future preferences are added.
|
||||
- Needs careful handling of disconnected LiveView mount so the default fallback is not written before the browser timezone arrives.
|
||||
|
||||
## Route 2 — Reuse `MusicLibrary.Secrets` as a persisted key-value store
|
||||
|
||||
Store the timezone under a key such as `user_timezone` or `current_timezone` using the existing encrypted `secrets` table.
|
||||
|
||||
### Pros
|
||||
|
||||
- Minimal implementation: no migration and no new schema/table.
|
||||
- Existing context already supports `store/2`, `get/1`, and update-on-conflict behaviour.
|
||||
- Meets the durability requirement for workers and controllers.
|
||||
|
||||
### Cons
|
||||
|
||||
- Semantically wrong: timezone is preference/configuration, not secret credential material.
|
||||
- Adds unnecessary encryption/decryption overhead to a frequently read value.
|
||||
- Makes `Secrets` responsible for both credentials and non-sensitive preferences, weakening architectural boundaries documented in `docs/architecture.md`.
|
||||
- Harder to document and reason about future settings.
|
||||
|
||||
## Route 3 — Store timezone only in the browser session/cookie/localStorage
|
||||
|
||||
Persist the timezone client-side or in the Phoenix session and read it during web requests.
|
||||
|
||||
### Pros
|
||||
|
||||
- No database migration.
|
||||
- Keeps browser-derived state near the browser.
|
||||
|
||||
### Cons
|
||||
|
||||
- Does not satisfy the requirement for background operations such as the records-on-this-day email worker, because Oban jobs do not have a browser session.
|
||||
- API callers and other server-side operations may not have access to the browser session.
|
||||
- Server restarts or cookie/session issues can reintroduce fallback-only behaviour.
|
||||
|
||||
## Route 4 — Runtime dynamic Oban Cron timezone
|
||||
|
||||
Attempt to make the Oban cron plugin's timezone follow the stored timezone so the daily records-on-this-day email runs at the stored local 7 AM rather than at the configured default timezone.
|
||||
|
||||
### Pros
|
||||
|
||||
- Aligns both the email date calculation and the send time with the stored timezone.
|
||||
|
||||
### Cons
|
||||
|
||||
- Oban OSS cron configuration is static at boot; dynamic runtime cron updates are an Oban Pro feature.
|
||||
- Reconfiguring/restarting cron in the application would be more complex and riskier than necessary for a single daily email.
|
||||
- Operational behaviour would be harder to verify and roll back.
|
||||
|
||||
## Final selected route
|
||||
|
||||
Use **Route 1** for persistence, plus a **self-scheduled Oban workflow** for the daily digest rather than attempting to mutate `Oban.Plugins.Cron` at runtime.
|
||||
|
||||
The schedule design should remove the static daily `SendRecordsOnThisDayEmail` cron entry and replace it with a scheduler that inserts an Oban job at the next local 7 AM according to the stored timezone. The schedule must be bootstrapped on application startup, rescheduled whenever the stored timezone changes, and rescheduled after each scheduler run. The email-sending job should keep retrying independently from the scheduling job so future daily scheduling is not lost when a Mailgun delivery attempt fails.
|
||||
Reference in New Issue
Block a user