Skip to content

fix(api): serialise stored timestamps as UTC so clients render the right time - #135

Merged
lopatnov merged 5 commits into
mainfrom
refactor/maintain-utc-timestamps
Sep 5, 2026
Merged

fix(api): serialise stored timestamps as UTC so clients render the right time#135
lopatnov merged 5 commits into
mainfrom
refactor/maintain-utc-timestamps

Conversation

@lopatnov

@lopatnov lopatnov commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Every DateTime this app persists is UTC, but SQL Server's datetime2 carries no zone, so EF Core hands stored values back as DateTimeKind.Unspecified and the "O"/"o" format specifier then omits the trailing Z — a browser reads an ISO-8601 date-time with no zone as local time, so every stored timestamp (article publish dates, comment and invite dates, admin user/report lists) rendered shifted by the viewer's UTC offset
  • The same article proved the bug live: pushed over the update stream its DateTime is still Kind=Utc and kept its Z so it rendered correctly, then jumped by the viewer's offset once a reload served the identical instant from the database
  • Adds Timestamps.ToIsoUtc (pins the kind to UTC before formatting — a no-op on an already-UTC value) and routes every mapper (AdminMapper, FeedItemMapper, FeedServiceImpl.Comments.cs, SubscriptionMapper) through it instead of each call site picking "o"/"O" on a value whose kind depends on where it came from

Related issue

Found by architect during this cycle's /maintain refactor analysis.

Type of change

  • Bug fix

Checklist

  • dotnet build --configuration Release passes with 0 errors, 0 warnings
  • cd src/pressmark-web && npm run build passes with 0 TypeScript errors (unaffected by this branch)
  • New behavior is covered by tests — TimestampsTests unit-tests ToIsoUtc directly: trailing Z for both DateTimeKind.Unspecified and DateTimeKind.Utc, that both produce the same wire format for the same instant (the actual bug), and "" for the nullable overload's null case; 123/123 backend tests pass
  • All UI strings use t('ns:key') — no strings touched

Notes for reviewers

The riskiest part of this change is AdminMapper.UserInfoProjection, an Expression<Func<...>> consumed inside a live EF query (AdminServiceImpl.Users.cs's ListUsers). I independently re-verified (not just re-read the architect agent's own claim) that it still translates: built a throwaway test using IQueryable.ToQueryString() against a UseSqlServer context with a fake connection string (translation doesn't require a live connection) and confirmed the generated SQL selects the raw created_at column — ToIsoUtc() is client-evaluated post-materialization on the top-level projection, exactly as claimed, same as the ToString("O") it replaces. That throwaway test was deleted before this PR — it's not part of the diff.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg


Generated by Claude Code

…ght time

Every DateTime this app persists is UTC, but SQL Server's datetime2 carries no
zone, so EF Core returns it as DateTimeKind.Unspecified and the "O" specifier then
omits the trailing Z. A browser reads an ISO-8601 date-time with no zone as local
time, so every stored timestamp — article publish dates, comment and invite dates,
the admin user/report lists — rendered shifted by the viewer's UTC offset.

The same article proved it: pushed over the update stream its DateTime is still
Kind=Utc and kept its Z, so it rendered correctly, then jumped by the offset once
a reload served the identical instant from the database.

Route the mappers through Timestamps.ToIsoUtc, which pins the kind before
formatting (a no-op on a value already marked UTC) instead of each call site
picking "o"/"O" on a value whose kind depends on where it came from.

Verified that AdminMapper.UserInfoProjection still translates: it is the top-level
projection of its query, which EF Core evaluates client-side, same as the
ToString("O") it replaces.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
Adds a focused unit test for the fix in ef8bfbe:
proves ToIsoUtc emits a trailing Z for both a DateTimeKind.Unspecified value (what
EF Core hands back for a stored datetime2) and a DateTimeKind.Utc value, and that
the two produce the identical wire format for the same instant — that disagreement
was the actual bug, since a browser reads a zone-less ISO-8601 string as local
time. Also covers the nullable overload's "" for null.

No database needed; runs standalone via `dotnet test`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 22 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 4ed20cdb-2177-4e87-98fa-2377d3be699a

📥 Commits

Reviewing files that changed from the base of the PR and between e240f45 and 7096ec3.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/Pressmark.Api.Tests/TimestampsTests.cs
  • src/Pressmark.Api/Services/AdminMapper.cs
  • src/Pressmark.Api/Services/FeedItemMapper.cs
  • src/Pressmark.Api/Services/FeedServiceImpl.Comments.cs
  • src/Pressmark.Api/Services/SubscriptionMapper.cs
  • src/Pressmark.Api/Services/Timestamps.cs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
@lopatnov
lopatnov marked this pull request as ready for review September 5, 2026 07:50
Independent architect review found the existing tests only asserted
EndsWith("Z") and Utc/Unspecified equality, which can't distinguish the
correct SpecifyKind-based fix from a plausible-but-wrong alternative
(calling ToUniversalTime() first) that shifts an Unspecified-kind value by
the server's local offset before formatting — verified empirically: that
wrong implementation still passes every existing assertion under TZ=UTC,
which is what CI runs. Pinning the literal expected string closes the gap
without depending on the test runner's timezone.

Also: the CHANGELOG entry named article/comment/invite/admin-panel
timestamps but missed SubscriptionMapper's lastFetchedAt, which is
user-visible on the Subscriptions page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
@gitar-bot

gitar-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown
Code Review ✅ Approved

Fixes timestamp serialization by routing all mappers through Timestamps.ToIsoUtc to pin DateTimeKind to UTC before formatting, ensuring stored timestamps render with the correct timezone offset on clients instead of shifting by the viewer's local UTC offset. Test coverage added for UTC-kind pinning. No issues found.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@lopatnov
lopatnov merged commit 0ea96fe into main Sep 5, 2026
14 checks passed
@sonarqubecloud

sonarqubecloud Bot commented Sep 5, 2026

Copy link
Copy Markdown

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.

2 participants