Skip to content

feat(settings): add userspace timezone selection and a device clock - #3163

Merged
vpetersson merged 7 commits into
masterfrom
feature/timezone-selection
Jul 8, 2026
Merged

feat(settings): add userspace timezone selection and a device clock#3163
vpetersson merged 7 commits into
masterfrom
feature/timezone-selection

Conversation

@vpetersson

@vpetersson vpetersson commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Issues Fixed

Description

Adds an operator-selectable Timezone in Settings that drives all schedule evaluation and time display, plus a live device clock on the System Info page.

The timezone is stored in anthias.conf and applied entirely in userspace — no host access, no reboot. It resolves with the precedence anthias.confTZ env → /etc/timezoneUTC, and every candidate is validated so a bad value can never crash-loop the settings module. A per-request middleware re-activates it so a save takes effect immediately across the UI, the REST API, and the server-evaluated viewer playlist; the viewer's own scheduler re-activates it on startup and on every reload.

This matters most on balenaOS, where the host is permanently UTC and there is no host-level timezone to inherit — so today every balena device schedules and displays in UTC. Setting the timezone in userspace is in fact balena's own recommended approach, and it's the only way to get correct local-time scheduling there.

Highlights:

  • New timezone device setting; dropdown populated from the IANA zone list.
  • Wired through both write paths (HTML form + /api/v2/device_settings) and the v2 serializers, with validation.
  • System Info gains a Device time card that ticks client-side but is seeded from the server instant (so a wrong device clock is revealed, not masked) and renders in the device's own zone via Intl.
  • /api/v2/info gains a time object (iso / timezone / offset).

Out of scope (deliberately): setting the absolute clock or a custom NTP server from the web UI. On balena that isn't possible from the app container — the Supervisor API has no time endpoint, and NTP is managed by the host (config.json ntpServers / DHCP). Those asks from #1755/#2996 are left as a possible docker-compose-only follow-up.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

- Add an operator-selectable Timezone in Settings, stored in
  anthias.conf and applied live per request (config -> TZ env ->
  /etc/timezone -> UTC), so scheduling and time display use local
  time with no host access — the only way to get non-UTC on balena.
- Activate the resolved zone in the viewer's scheduler on startup and
  every reload so play-windows track the server.
- Validate the zone on both write paths and in the serializer so a
  bad value can never crash-loop the settings module.
- Show a live device clock (seeded from the server instant) on System
  Info and expose a `time` object on /api/v2/info.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vpetersson
vpetersson requested a review from a team as a code owner July 8, 2026 17:35
@vpetersson vpetersson self-assigned this Jul 8, 2026
@vpetersson
vpetersson requested a review from Copilot July 8, 2026 17:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an operator-selectable, userspace-only timezone setting that affects scheduling/time display across the server + viewer, and adds a live “Device time” clock in System Info plus a /api/v2/info time payload.

Changes:

  • Add a timezone device setting (HTML + API v2) with IANA dropdown + validation, persisted in anthias.conf.
  • Resolve/activate the effective timezone (config → TZ env → host /etc/timezone → UTC) per-request via new middleware, and in the viewer on startup/reload.
  • Add device clock display to System Info and expose device time/zone/offset via /api/v2/info.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_template_views.py Adds assertions for the new “Device time” card and timezone dropdown rendering.
tests/test_scheduler.py Adds an end-to-end test verifying scheduling follows the active Django timezone.
tests/test_django_timezone.py Adds coverage for timezone validation/resolution and the new activation middleware.
src/anthias_viewer/init.py Activates the resolved timezone in the viewer on settings load/reload.
src/anthias_server/settings.py Adds timezone to defaults/configurable settings persisted to anthias.conf.
src/anthias_server/lib/timezone.py Introduces per-request timezone activation middleware with safe fallback.
src/anthias_server/django_project/settings.py Implements timezone validation + resolution precedence; wires middleware; sets TIME_ZONE from resolver.
src/anthias_server/app/views.py Validates and saves timezone from the Settings HTML form.
src/anthias_server/app/templates/system_info.html Adds “Device time” stat card and client-side ticking seeded from server time.
src/anthias_server/app/templates/settings.html Adds timezone dropdown to Settings page.
src/anthias_server/app/page_context.py Supplies device time info and timezone dropdown options to templates.
src/anthias_server/api/views/v2.py Adds timezone to device settings API + adds /api/v2/info.time.
src/anthias_server/api/tests/test_v2_endpoints.py Adds API tests for timezone patching/validation and settings payload fields.
src/anthias_server/api/serializers/v2.py Adds timezone fields + validation to v2 device settings serializers.
ansible/roles/anthias/files/anthias.conf Documents and adds the new timezone config key.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/anthias_server/app/page_context.py
Comment thread src/anthias_server/api/serializers/v2.py
Comment thread tests/test_django_timezone.py Outdated
- Pair Uptime + Device time in one grid row (and the two chart cards
  in theirs) so the short clock card no longer stretches to a chart
  card's height and leaves a dead vertical band.
- Humanise displayed IANA names (underscore -> space) in the timezone
  dropdown and the device-clock sub-label; stored value + Intl data
  attribute keep the real id.
- Refresh the Display & playback section blurb to mention timezone.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vpetersson

Copy link
Copy Markdown
Contributor Author

End-to-end validation — Raspberry Pi 5 (latest-pi5)

Deployed this branch's code onto a Pi 5 (4 GB, latest-pi5 stack) via a full-src overlay, restarted the server/celery/viewer containers, exercised the feature, then restored the device to its pristine image. All green.

Functional

Check Result
Server boots with the change /api/v2/info → 200
Baseline, no override ✅ follows host → Europe/London / UTC+01:00 (BST)
New time object on /api/v2/info iso + timezone + offset
Live override → America/New_York ✅ persisted; /api/v2/info immediately reported UTC-04:00 with no restart (per-request activation)
Scheduling honours the active zone (on-device) ✅ same asset + same UTC instant: is_active=True at 09:00 Berlin (in a 09:00–17:00 window), False at 02:00 Chicago
Invalid zone (Mars/Phobos) ✅ rejected 400
Clear override ✅ falls back to the host zone
Viewer container ✅ healthy, regenerating the playlist

UI/UX (screenshotted via headless Chromium against the device)

Ran a short design review of the two new surfaces and applied three fixes (second commit):

  • Device time card was stretching to the tall Memory card's height, leaving a dead vertical band — reordered the System Info grid so the two short text cards (Uptime + Device time) share a row and the two chart cards share theirs.
  • Humanised IANA names (underscore → space) in the timezone dropdown and the clock sub-label; the stored value and the Intl data attribute keep the real id.
  • Refreshed the Display & playback section blurb to mention timezone.

Settings shows the Timezone select styled identically to Audio output / Date format; System Info shows a live-ticking Device time card seeded from the server instant (so a wrong device clock is revealed, not masked) and rendered in the device's own zone.

Not covered

x86 was not tested end-to-end (that testbed was in use); the x86 checklist box is left unchecked.

Use RequestFactory + HttpResponse instead of object()/str stubs so the
TimezoneActivationMiddleware test satisfies the CI mypy signature check.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- page_context: snapshot the local instant + zone name once so the
  clock's iso/offset can't straddle a second boundary.
- v2 serializer: drop the {!r} quoting in the invalid-timezone message
  so it matches the HTML-path wording.
- middleware test: compare the post-request zone to a captured
  baseline instead of a hardcoded name (robust when the host default
  is Europe/Stockholm).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Comment thread src/anthias_server/app/page_context.py
Comment thread src/anthias_server/app/templates/system_info.html
…now()

Fall back to Date.now() for the tick baseline where performance.now()
is unavailable (older/embedded browsers), so the clock never throws.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment thread src/anthias_server/api/views/v2.py
Comment thread src/anthias_server/django_project/settings.py Outdated
Comment thread src/anthias_server/app/page_context.py Outdated
- Emit device time `iso` at seconds precision (page_context + v2 info)
  so microseconds can't make an older JS Date.parse() return NaN and
  stall the live clock.
- get_configured_time_zone(): build the default config path from the
  shared CONFIG_DIR / CONFIG_FILE constants instead of hard-coding it.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Comment thread src/anthias_server/settings.py Outdated
Comment thread src/anthias_server/django_project/settings.py Outdated
Comment thread src/anthias_server/app/page_context.py Outdated
Comment thread src/anthias_server/api/views/v2.py Outdated
Comment thread src/anthias_server/app/views.py Outdated
Comment thread src/anthias_server/api/serializers/v2.py Outdated
Comment thread src/anthias_server/api/tests/test_v2_endpoints.py Outdated
- Extract format_utc_offset() into anthias_server.lib.timezone and use
  it from both page_context and the /api/v2/info clock instead of
  duplicating the formatting.
- Reword the "follow the host" comments/docstrings to state the actual
  blank-value fallback: TZ env -> /etc/timezone -> UTC.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@sonarqubecloud

sonarqubecloud Bot commented Jul 8, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

@vpetersson

Copy link
Copy Markdown
Contributor Author

Copilot review is clean ✅ — its latest pass on cef1c946 reviewed all 15 changed files and generated no comments, and every finding from the earlier passes has been addressed and its thread resolved.

Fixes made in response to Copilot across the review rounds:

  • Clock seed (device_time.iso + /api/v2/info) emits seconds precision so microseconds can't make an older JS Date.parse() return NaN and stall the live clock.
  • The System Info clock script falls back to Date.now() where performance.now() is unavailable.
  • Extracted format_utc_offset() into anthias_server.lib.timezone, shared by the page and the API (removed the duplicate).
  • get_configured_time_zone() builds its default path from the shared CONFIG_DIR / CONFIG_FILE constants instead of hard-coding it.
  • Snapshot the local instant + zone name once when building device_time (no intra-second skew).
  • Reworded the "follow the host" comments to state the real blank-value fallback: TZ env → /etc/timezone → UTC.

CI is green across the board (mypy, python + typescript tests, linter, OpenAPI schema, build, CodeQL, SonarCloud). Ready for a maintainer's review.

@vpetersson
vpetersson merged commit b28ce96 into master Jul 8, 2026
11 checks passed
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.

Add support for changing timezone settings via the Settings page.

2 participants