Files
music_library/backlog/docs/doc-28 - ML-198-Research-Store-user-timezone-implementation-routes.md
T
2026-05-27 01:11:07 +03:00

6.9 KiB

id, title, type, created_date, updated_date, tags
id title type created_date updated_date tags
doc-28 ML-198 Research: Store user timezone implementation routes specification 2026-05-26 21:55 2026-05-26 22:02
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.