fix: compute contract date client-side for local timezone #10

Merged
forgejo_admin merged 1 commit from fix/9-contract-date-timezone into main 2026-03-24 19:52:51 +00:00

Summary

The contract signing page date was computed during SSR in the server's UTC timezone. Since the k8s pod runs UTC, Utah users (MDT, UTC-6) saw tomorrow's date after 6 PM local time. This moves date computation into onMount() so it runs client-side with the user's local timezone.

Changes

  • src/routes/contract/[token]/+page.svelte: changed dateStr from a const computed at SSR time to a $state variable populated in onMount(). SSR renders an empty string; client hydration fills in the correct local date. Both usages (date input field and success overlay) are covered.

Test Plan

  • Tests pass locally (12/12)
  • npm run build succeeds
  • Manual verification: open contract page after 6 PM MDT -- date should show today, not tomorrow
  • Manual verification: success overlay after signing shows correct local date

Review Checklist

  • Passed automated review-fix loop
  • No secrets committed
  • No unnecessary file changes
  • Commit messages are descriptive
  • Closes #9
## Summary The contract signing page date was computed during SSR in the server's UTC timezone. Since the k8s pod runs UTC, Utah users (MDT, UTC-6) saw tomorrow's date after 6 PM local time. This moves date computation into `onMount()` so it runs client-side with the user's local timezone. ## Changes - `src/routes/contract/[token]/+page.svelte`: changed `dateStr` from a `const` computed at SSR time to a `$state` variable populated in `onMount()`. SSR renders an empty string; client hydration fills in the correct local date. Both usages (date input field and success overlay) are covered. ## Test Plan - [x] Tests pass locally (12/12) - [x] `npm run build` succeeds - [ ] Manual verification: open contract page after 6 PM MDT -- date should show today, not tomorrow - [ ] Manual verification: success overlay after signing shows correct local date ## Review Checklist - [x] Passed automated review-fix loop - [x] No secrets committed - [x] No unnecessary file changes - [x] Commit messages are descriptive ## Related - Closes #9
The date on the contract signing page was computed during SSR in the
server's UTC timezone. Since the k8s pod runs UTC, Utah users (MDT,
UTC-6) saw tomorrow's date after 6 PM local time.

Move date computation into onMount() so it runs client-side with the
user's local timezone. SSR renders an empty string, then hydration
fills in the correct local date.

Closes #9

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Review: Approved

Diff: 1 file, +8/-4. Minimal and surgical.

Correctness: const dateStr was computed once during SSR (UTC). Now $state('') + onMount() ensures the date is computed client-side in the user's local timezone. SSR renders empty string, hydration fills correct date. Both usages (date input on line 462, success overlay on line 494) read from the same reactive dateStr.

No issues found:

  • onMount already imported (line 2) -- no new imports needed
  • Multiple onMount calls are valid in Svelte (existing one at line 117 for signature pad)
  • Build passes, all 12 tests pass
  • No secrets, no unrelated changes

LGTM.

## Review: Approved **Diff**: 1 file, +8/-4. Minimal and surgical. **Correctness**: `const dateStr` was computed once during SSR (UTC). Now `$state('')` + `onMount()` ensures the date is computed client-side in the user's local timezone. SSR renders empty string, hydration fills correct date. Both usages (date input on line 462, success overlay on line 494) read from the same reactive `dateStr`. **No issues found**: - `onMount` already imported (line 2) -- no new imports needed - Multiple `onMount` calls are valid in Svelte (existing one at line 117 for signature pad) - Build passes, all 12 tests pass - No secrets, no unrelated changes LGTM.
Author
Owner

PR #10 Review

DOMAIN REVIEW

Stack: SvelteKit (Svelte 5 runes), TypeScript, adapter-node, SSR-enabled.

Fix correctness: The root cause is correctly identified. new Date() at module scope executes during SSR on the k8s pod, which runs UTC. For users in MDT (UTC-6), any time after 6 PM local would produce tomorrow's date. Moving into onMount() guarantees client-side execution with the browser's local timezone. The formatting options (en-US, year: 'numeric', month: 'long', day: 'numeric') are unchanged.

SSR empty-string safety: During SSR, dateStr is ''. Both usages are safe:

  • The date input field (line 462) is inside .signature-area, which is gated behind class:visible={agreed} -- a user interaction required before it renders visibly.
  • The success overlay (line 494) only renders when showSuccess is true, which requires a successful POST -- exclusively a client-side event.

No hydration mismatch risk: the server renders '', and the client fills in the date before the user can see either element. Clean.

Multiple onMount calls: The component now has two onMount() calls (lines 26 and 117). Svelte 5 supports multiple onMount registrations per component -- each callback runs independently. No issue.

Accessibility: The date input (line 462) has readonly and a <label> with for="signDate". No regression.

BLOCKERS

None.

This is a bug fix that relocates existing logic from SSR to client-side. No new functionality is introduced -- the Date constructor and toLocaleDateString call are identical. The 12 existing validation tests cover the server-side signing logic. A unit test for "which timezone does new Date() run in" would be testing the browser/Node runtime, not application logic. No blocker applies.

NITS

  1. Two separate onMount calls could be consolidated (lines 26-32 and 117-130). Both run on mount. Merging them into a single onMount callback would be slightly cleaner and make the component's lifecycle easier to scan. Non-blocking -- the current approach works correctly.

  2. Inline date formatting on line 156: The "already signed" view formats player.contract_signed_at with an inline new Date(...).toLocaleDateString(...). This is fine because it formats a stored timestamp (not "now"), but it duplicates the same locale/options object used for dateStr. A shared formatter constant could reduce repetition. Out of scope for this PR -- noting for future cleanup.

SOP COMPLIANCE

  • Branch named after issue: fix/9-contract-date-timezone references issue #9
  • PR body follows template: Summary, Changes, Test Plan, Related sections present
  • Related references issue: Closes #9
  • Related references plan slug: No plan slug referenced. This is a standalone bug fix -- acceptable if no plan governs this work.
  • No secrets committed
  • No unnecessary file changes: 1 file, 8 additions, 4 deletions -- tightly scoped
  • Commit messages are descriptive
  • Tests pass (12/12 per PR body)

PROCESS OBSERVATIONS

  • Change failure risk: Low. The fix is minimal and isolated -- 4 lines removed, 8 lines added, all in one component. The logic is identical; only the execution context changes.
  • Deployment frequency: No impediment. Single-file change, no migration, no config change.
  • Documentation: The PR body clearly explains the root cause (UTC pod timezone vs. MDT user timezone) and the fix rationale. Good.

VERDICT: APPROVED

## PR #10 Review ### DOMAIN REVIEW **Stack**: SvelteKit (Svelte 5 runes), TypeScript, adapter-node, SSR-enabled. **Fix correctness**: The root cause is correctly identified. `new Date()` at module scope executes during SSR on the k8s pod, which runs UTC. For users in MDT (UTC-6), any time after 6 PM local would produce tomorrow's date. Moving into `onMount()` guarantees client-side execution with the browser's local timezone. The formatting options (`en-US`, `year: 'numeric', month: 'long', day: 'numeric'`) are unchanged. **SSR empty-string safety**: During SSR, `dateStr` is `''`. Both usages are safe: - The date input field (line 462) is inside `.signature-area`, which is gated behind `class:visible={agreed}` -- a user interaction required before it renders visibly. - The success overlay (line 494) only renders when `showSuccess` is true, which requires a successful POST -- exclusively a client-side event. No hydration mismatch risk: the server renders `''`, and the client fills in the date before the user can see either element. Clean. **Multiple `onMount` calls**: The component now has two `onMount()` calls (lines 26 and 117). Svelte 5 supports multiple `onMount` registrations per component -- each callback runs independently. No issue. **Accessibility**: The date input (line 462) has `readonly` and a `<label>` with `for="signDate"`. No regression. ### BLOCKERS None. This is a bug fix that relocates existing logic from SSR to client-side. No new functionality is introduced -- the `Date` constructor and `toLocaleDateString` call are identical. The 12 existing validation tests cover the server-side signing logic. A unit test for "which timezone does `new Date()` run in" would be testing the browser/Node runtime, not application logic. No blocker applies. ### NITS 1. **Two separate `onMount` calls could be consolidated** (lines 26-32 and 117-130). Both run on mount. Merging them into a single `onMount` callback would be slightly cleaner and make the component's lifecycle easier to scan. Non-blocking -- the current approach works correctly. 2. **Inline date formatting on line 156**: The "already signed" view formats `player.contract_signed_at` with an inline `new Date(...).toLocaleDateString(...)`. This is fine because it formats a stored timestamp (not "now"), but it duplicates the same locale/options object used for `dateStr`. A shared formatter constant could reduce repetition. Out of scope for this PR -- noting for future cleanup. ### SOP COMPLIANCE - [x] Branch named after issue: `fix/9-contract-date-timezone` references issue #9 - [x] PR body follows template: Summary, Changes, Test Plan, Related sections present - [x] Related references issue: `Closes #9` - [ ] Related references plan slug: No plan slug referenced. This is a standalone bug fix -- acceptable if no plan governs this work. - [x] No secrets committed - [x] No unnecessary file changes: 1 file, 8 additions, 4 deletions -- tightly scoped - [x] Commit messages are descriptive - [x] Tests pass (12/12 per PR body) ### PROCESS OBSERVATIONS - **Change failure risk**: Low. The fix is minimal and isolated -- 4 lines removed, 8 lines added, all in one component. The logic is identical; only the execution context changes. - **Deployment frequency**: No impediment. Single-file change, no migration, no config change. - **Documentation**: The PR body clearly explains the root cause (UTC pod timezone vs. MDT user timezone) and the fix rationale. Good. ### VERDICT: APPROVED
forgejo_admin deleted branch fix/9-contract-date-timezone 2026-03-24 19:52:51 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/westside-contracts!10
No description provided.