Skip to content

Render timestamps in each viewer's local time zone (store UTC, display per-user) #455

Description

@sierragolflima

Type

Modification / feature request (not a defect). Timestamp storage stays UTC and is
unchanged
; this adds a per-user display conversion so each viewer sees timestamps in
their own local time, correctly, wherever they are.

Motivation

Timestamps in the portal (e.g. atioz.meshweaver.cloud) currently render in UTC — 1–2 hours
behind Swiss wall-clock time (CET/CEST). Today the display code calls .ToLocalTime() /
.LocalDateTime, but under Blazor Server "local" resolves to the server process zone,
and the deployment container runs UTC, so the conversion is a no-op.

The user base is in Switzerland today but may operate from other regions (e.g. the US) in
future. A single fixed display zone would then show Swiss time to a US user. The goal is a
solution that is stable across all time zones: correctness follows the viewer, not the
deployment or the server.

Proposal

Store UTC (unchanged); resolve the display zone per user; convert through one central
seam. Named IANA zones only — never fixed offsets — so DST is automatic and per-region
(US and EU DST switch on different dates).

  1. Source of truth — a TimeZoneId (IANA) field on the User node (UserNodeType.cs),
    e.g. America/New_York, Europe/Zurich. Mesh data: auditable, cross-device, editable
    in user settings, deterministically testable, and available anywhere the user identity
    is (including server-side/hub render paths that have no browser, via AccessContext).

  2. Default population by browser detection — when the field is empty (first sign-in),
    detect Intl.DateTimeFormat().resolvedOptions().timeZone via JS interop in
    OnAfterRenderAsync (the sanctioned DOM-side async carve-out) and write it once to
    the profile. Fallback chain: configured Display:TimeZone default → UTC. "Write only if
    empty" prevents a travelling user's device from overwriting their chosen home zone; an
    explicit setting always wins.

  3. One central seam ToDisplayTime(DateTimeOffset) (+ a small read-only timestamp
    format helper): resolves the current user's TimeZoneId (AccessContext → profile,
    cached per circuit in an instance scoped service — not static), then
    TimeZoneInfo.ConvertTime(utc, zone). Synchronous; no async on the render path.

  4. Route every render site through the seam (replaces the current server-local reads):

    File Line Current
    src/MeshWeaver.AI/ThreadMessageLayoutAreas.cs 368 started.ToLocalTime()
    src/MeshWeaver.Graph/VersionLayoutArea.cs 76 version.LastModified.LocalDateTime
    src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs 513 node.CreatedDate.ToLocalTime()
    src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs 520 node.LastModified.ToLocalTime()
    src/MeshWeaver.Blazor.Portal/Components/NotificationCenterPanel.razor 72 notif.CreatedAt.LocalDateTime
    src/MeshWeaver.Blazor.Portal/Components/NotificationCenterPanel.razor 380 ts.LocalDateTime
    src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor 42 thread.LastModified.LocalDateTime
    src/MeshWeaver.Maui/MauiViewPack.cs 2962 t.ToLocalTime()

Behaviour — one stored instant, two viewers

Stored: 2026-07-13T16:00:00Z (summer) / 2026-01-13T16:00:00Z (winter). Same node,
each render resolves the viewer's zone:

Viewer (TimeZoneId) July render (DST) January render
Europe/Zurich 18:00 (CEST, +2) 17:00 (CET, +1)
America/New_York 12:00 (EDT, −4) 11:00 (EST, −5)

The zone follows the viewer, not the author: a CH-authored entry still renders in New
York time for a New York viewer. "US time" is the viewer's specific IANA zone
(America/Los_AngelesAmerica/New_York), resolved per user — not one assumed US zone.

Why per-user (and not the alternatives)

  • Not a fixed configured zone — would show Swiss time to a US user; fails the moment
    users span regions.
  • Not a container TZ env var — ambient, silently reverts where unset, and shifts
    server-side reads/logs; also single-zone only.
  • Per-user resolution through one seam is stable everywhere: new regions need zero
    code/config change, and server-side render paths stay correct because the zone rides on
    the user identity, not the Blazor circuit.

Scope / impact

  • src/ framework change: a TimeZoneId field on the User content type + settings
    picker; the ToDisplayTime seam + per-circuit scoped resolver (DI singleton, no static
    state); one-time browser detection via OnAfterRenderAsync; the 8 call sites above;
    Display:TimeZone kept only as the fallback default.
  • Storage, serialization, versioning, sorting, and logging are untouched — all remain UTC.
  • No data migration; existing stored timestamps render correctly under the conversion.

Acceptance criteria

  • A stored 2026-07-13T16:00:00Z renders as 18:00 for a Europe/Zurich user and 12:00
    for an America/New_York user, simultaneously, from the same node.
  • DST is applied automatically via named zones (the January cases above), with no code change.
  • No render site reads server-local time (.ToLocalTime() / .LocalDateTime) any longer.
  • A user with no stored TimeZoneId is defaulted from the browser on first sign-in; a user
    can override the zone in settings and the override persists across sessions/devices.
  • Rendering is deterministic in tests: set the profile TimeZoneId, assert the output —
    independent of the host machine's zone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions