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).
-
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).
-
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.
-
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.
-
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_Angeles ≠ America/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.
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).
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, editablein user settings, deterministically testable, and available anywhere the user identity
is (including server-side/hub render paths that have no browser, via
AccessContext).Default population by browser detection — when the field is empty (first sign-in),
detect
Intl.DateTimeFormat().resolvedOptions().timeZonevia JS interop inOnAfterRenderAsync(the sanctioned DOM-side async carve-out) and write it once tothe profile. Fallback chain: configured
Display:TimeZonedefault → UTC. "Write only ifempty" prevents a travelling user's device from overwriting their chosen home zone; an
explicit setting always wins.
One central seam
ToDisplayTime(DateTimeOffset)(+ a small read-only timestampformat 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.Route every render site through the seam (replaces the current server-local reads):
src/MeshWeaver.AI/ThreadMessageLayoutAreas.csstarted.ToLocalTime()src/MeshWeaver.Graph/VersionLayoutArea.csversion.LastModified.LocalDateTimesrc/MeshWeaver.Graph/MeshNodeLayoutAreas.csnode.CreatedDate.ToLocalTime()src/MeshWeaver.Graph/MeshNodeLayoutAreas.csnode.LastModified.ToLocalTime()src/MeshWeaver.Blazor.Portal/Components/NotificationCenterPanel.razornotif.CreatedAt.LocalDateTimesrc/MeshWeaver.Blazor.Portal/Components/NotificationCenterPanel.razorts.LocalDateTimesrc/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razorthread.LastModified.LocalDateTimesrc/MeshWeaver.Maui/MauiViewPack.cst.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:
TimeZoneId)Europe/Zurich18:00(CEST, +2)17:00(CET, +1)America/New_York12: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_Angeles≠America/New_York), resolved per user — not one assumed US zone.Why per-user (and not the alternatives)
users span regions.
TZenv var — ambient, silently reverts where unset, and shiftsserver-side reads/logs; also single-zone only.
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: aTimeZoneIdfield on the User content type + settingspicker; the
ToDisplayTimeseam + per-circuit scoped resolver (DI singleton, no staticstate); one-time browser detection via
OnAfterRenderAsync; the 8 call sites above;Display:TimeZonekept only as the fallback default.Acceptance criteria
2026-07-13T16:00:00Zrenders as18:00for aEurope/Zurichuser and12:00for an
America/New_Yorkuser, simultaneously, from the same node..ToLocalTime()/.LocalDateTime) any longer.TimeZoneIdis defaulted from the browser on first sign-in; a usercan override the zone in settings and the override persists across sessions/devices.
TimeZoneId, assert the output —independent of the host machine's zone.