feat(settings): add userspace timezone selection and a device clock - #3163
Conversation
- 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]>
There was a problem hiding this comment.
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
timezonedevice setting (HTML + API v2) with IANA dropdown + validation, persisted inanthias.conf. - Resolve/activate the effective timezone (config →
TZenv → 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.
- 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]>
End-to-end validation — Raspberry Pi 5 (
|
| 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
Intldata 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]>
…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]>
- 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]>
- 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]>
|
|
Copilot review is clean ✅ — its latest pass on Fixes made in response to Copilot across the review rounds:
CI is green across the board (mypy, python + typescript tests, linter, OpenAPI schema, build, CodeQL, SonarCloud). Ready for a maintainer's review. |



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.confand applied entirely in userspace — no host access, no reboot. It resolves with the precedenceanthias.conf→TZenv →/etc/timezone→UTC, 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 everyreload.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:
timezonedevice setting; dropdown populated from the IANA zone list./api/v2/device_settings) and the v2 serializers, with validation.Intl./api/v2/infogains atimeobject (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