Skip to content

Render notification times in local time instead of UTC - #629

Open
badbread wants to merge 2 commits into
mainfrom
fix/notification-local-time
Open

Render notification times in local time instead of UTC#629
badbread wants to merge 2 commits into
mainfrom
fix/notification-local-time

Conversation

@badbread

@badbread badbread commented Sep 8, 2026

Copy link
Copy Markdown
Owner

What changed

Notification timestamps were formatted in UTC with a literal "UTC" suffix, so an
alert read on a phone in another zone showed the wrong time at a glance. The
rendering is now chosen per destination:

  • Discord: <t:UNIX:f> for a date-time (%datetime% and the legacy "at ..."
    wording), <t:UNIX:t> for %time%, <t:UNIX:d> for %date%. The text goes
    into the webhook's content field, which renders Discord markup, so every
    viewer's client shows their own local time.
  • Slack: <!date^UNIX^{date_short_pretty} at {time}|FALLBACK> (and the
    single-token forms for %date%/%time%), where FALLBACK is the server-zone
    rendering. The text goes into the incoming webhook's top-level text field,
    which renders that markup.
  • ntfy, Pushover, Telegram, generic webhook, and any unknown kind:
    %Y-%m-%d %H:%M:%S %Z in the server's configured zone, so the zone
    abbreviation is visible. %date% and %time% render without the suffix.

The zone comes from TZ, resolved once at startup by a new
crumb_common::config::server_tz() wrapper around the existing parse_tz_env
helper, stored as ApiConfig::server_tz, and threaded into the notification
engine the same way the go2rtc credentials already are. No new env keys.

The style is picked by channel_notify::time_style_for(kind, tz) and applied by
ChannelMessage::text_for(style) / token_map_for(style) /
rendered_title_for(style), so the template engine is not duplicated. The
zero-argument text() / rendered_title() had no remaining callers once every
dispatcher passed a style, so they were removed rather than left unused (an
unused method would fail the -D warnings clippy gate).

Stored event timestamps stay UTC. The generic webhook's JSON ts field also
stays a raw UTC instant, since that is a machine contract. There is no email
provider in the dispatcher, and the device push path does not format the
timestamp at all (the client renders it), so neither needed a change.

Docs

The alert-text editor in the admin console previously said "Times are UTC"; it
now states the per-provider rule and names the server's zone, and its live
preview stamps the sample %datetime% with the server's zone abbreviation
instead of "UTC". Also updated: docs-site/docs/notifications/index.md (new
"Times in alerts" section), the TZ row in
docs-site/docs/configuration/environment-reference.md, the alert-templating
bullet in docs/AI-INSTALL.md, the TZ comment in docker-compose.yml, a
docs/COMPONENT-MAP.md row, and a docs/DECISIONS.md entry recording why the
markup-vs-server-zone split was chosen over a per-user timezone setting.

How tested

Build-box gate green on 8fa62fb: cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test --workspace against a throwaway
Postgres, ending in GATE_OK fix/notification-local-time.

New unit tests in channel_notify.rs cover the Discord markup and its unix
seconds, the Slack markup with its server-zone fallback, and the zone rendering
of fixed UTC instants under America/Los_Angeles for both a winter (PST) and a
summer (PDT, which also shifts the calendar date) instant. The tests pass the
Tz in explicitly and never read the process environment.

Fixes #628

Every channel message formatted the event timestamp in UTC with a literal
"UTC" suffix, so an alert read on a phone in another zone showed the wrong
time at a glance.

Timestamps are now rendered per destination. Discord gets its own
<t:UNIX:f|t|d> markup and Slack gets <!date^UNIX^tokens|fallback> markup,
both of which those clients resolve in each viewer's own zone. ntfy,
Pushover, Telegram, the generic webhook, and any future kind render
"%Y-%m-%d %H:%M:%S %Z" in the server's configured zone, resolved once at
startup from TZ into ApiConfig::server_tz and threaded into the
notification engine the way the go2rtc credentials already are. The
%date%, %time% and %datetime% template tokens follow the same style.

Stored event timestamps stay UTC, and the generic webhook's JSON ts field
stays a raw UTC instant since it is a machine contract.

Signed-off-by: badbread <badbread@users.noreply.github.com>
Signed-off-by: badbread <badbread@users.noreply.github.com>
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.

Notification timestamps render in UTC instead of local time

1 participant