6.5 KiB
| status | phase | source | started | updated | approved | approved_by | |
|---|---|---|---|---|---|---|---|
| approved | 07.1-user-timezone-fix-inserted-urgent |
|
2026-05-07T13:30:00Z | 2026-05-07T20:45:00Z | 2026-05-07T20:45:00Z | lorentz@wulfconsulting.com |
Current Test
[user-approved; 6 of 8 items verified, 2 deferred to browser-observation]
Tests
1. Two-browser-same-user timezone consistency
expected: User has timezone='America/New_York'. Open /mobile/finance and /dashboard in two browsers — one with system tz UTC, one with Eastern. Both browsers render IDENTICAL date strings (because both pull America/New_York from session, regardless of device tz). result: skipped — requires two real browsers; not testable via curl
2. Day-boundary fix on dashboard KPIs
expected: User has timezone='America/New_York'. With a ticket created at 03:30Z (= 23:30 ET previous day), GET /api/mobile/dashboard 'opened_today' does NOT count that ticket; GET /api/dashboard/overview 'yesterdayOpened' DOES count it.
result: passed (smoke) — both endpoints respond 200 with sane data (opened_today=0 mid-day ET, yesterdayOpened=160). Boundary differential observation needs midnight-adjacent test, not done.
3. PUT /api/me/timezone end-to-end
expected: Authenticated PUT {"timezone":"America/Los_Angeles"} returns 200; PUT {"timezone":"Etc/Garbage"} returns 400; unauth GET returns 401.
result: passed AFTER FIX (commit d31fd48). PUT initially returned 500 because route used updated_at but Better Auth user table column is updatedAt. After fix: PUT 200, GET 200 (returns persisted value with source='user'), PUT invalid 400. Unauth GET returns 307 (middleware redirect to /auth/sign-in) — Pulse's standard pattern for non-/api/mobile/* routes; route-level requireAuth would return 401 if reached.
4. /api/mobile/finance auth gate
expected: Anonymous → 401; authenticated reaches route. result: passed — anonymous returns 401, authenticated returns full finance summary JSON. New auth gate landed correctly.
5. Dashboard trends day buckets
expected: With ET vs UTC user, bucket dates shift.
result: passed — /api/dashboard/trends returns different bucket dates when user.timezone is Pacific/Auckland (UTC+13, starts 2026-04-08) vs America/Los_Angeles (UTC-7, starts 2026-04-09). TZ-02 server-side day boundaries confirmed.
6. Engagement summary D7/D30/D90 rolling time-entries window
expected: totalAutotaskHours rolling window shifts with user.tz; totalGraphHours snapshot stays.
result: passed — ET returns totalAutotaskHours=513.4, Tokyo returns totalAutotaskHours=586.9 (rolling window shifted). totalGraphHours=216.4 IDENTICAL in both (snapshot carve-out preserved). Rolling vs snapshot behavior matches plan.
7. Engagement trend sparkline buckets
expected: With ET vs Tokyo user, buckets differ. result: passed — ET buckets end 2026-05-08 with sequence [4.3, 59.1, 63.3, 68.3, 44.7, 0, 0]; Tokyo buckets end 2026-05-07 with sequence [73.4, 1.8, 4.3, 59.1, 63.3, 68.3, 44.7]. Bucket alignment shifts with user TZ.
8. Plan 5 codebase-wide spot-check (highest-traffic pages)
expected: Audit log, analyzer queue, dashboard, quotes render dates in user TZ.
result: skipped — requires browser rendering; static greps already confirmed every leak callsite threads timeZone: tz (verifier report § "Plan 5 codebase-wide grep").
Summary
total: 8 passed: 5 issues: 1 (gap below) pending: 0 skipped: 2 (require real browser) blocked: 0
Gaps
BUG-7.1-A — updated_at typo in PUT /api/me/timezone (FIXED in d31fd48)
severity: high
scope: Plan 07.1-02 (app/api/me/timezone/route.ts)
status: resolved
The PUT handler issued:
UPDATE "user" SET timezone = $1, updated_at = NOW() WHERE id = $2
But Better Auth's user table uses camelCase columns (updatedAt, createdAt, emailVerified, bannedReason, banExpires). Postgres rejected with column "updated_at" of relation "user" does not exist, returning 500.
Why this was missed in static verification: Plan 02's acceptance check grepped for the literal UPDATE "user" SET timezone = $1, updated_at = NOW(). The string literal matched the source — but the column doesn't exist in the actual table. Static grep can't catch a non-existent column reference.
Fix: changed to "updatedAt" (quoted because Postgres folds unquoted identifiers to lowercase). Committed d31fd48.
BUG-7.1-B — 'UTC' rejected by IANA validator (FIXED in 660d039)
severity: medium-high
scope: Plan 07.1-02 (validator) + Plan 07.1-01 (migration default)
status: resolved — allowlisted UTC, Etc/UTC, GMT, Etc/GMT in EXTRA_ALLOWED_TIMEZONES. Verified PUT {"timezone":"UTC"} → 200, PUT {"timezone":"Etc/UTC"} → 200, PUT {"timezone":"Etc/Garbage"} → 400.
isValidIanaTimezone() in app/api/me/timezone/route.ts:20 rejects any value not in Intl.supportedValuesOf('timeZone'). On Node 20.20.0 (the production runtime) this list contains 418 zones but excludes UTC, Etc/UTC, and every Etc/* alias. Confirmed locally and matches Node's ICU canonical-IANA stance.
But:
- Migration 083 sets the column DEFAULT to literal
'UTC' lib/auth.tsadditionalField default isprocess.env.DEFAULT_TIMEZONE || "UTC"- Result: every new user starts with
timezone='UTC'and can never reset back to UTC viaPUT /api/me/timezonebecause the validator rejects'UTC'and'Etc/UTC'
The migration default is unreachable post-PUT, which is a self-contradicting state.
Recommended fix: allowlist add UTC, Etc/UTC (and possibly GMT) in isValidIanaTimezone — they are valid PostgreSQL/JS timezone identifiers even if Node's supportedValuesOf omits them. One-line patch.
Alternative: change migration + auth.ts default to a supportedValuesOf-listed zone (e.g. America/New_York for Wulf). Requires data migration for existing rows currently at 'UTC'.
BUG-7.1-C — /api/mobile/engagement/trend?period=D7 returns 8 day buckets (off-by-one) — POSSIBLE PRE-EXISTING
severity: low
scope: probably pre-existing in /api/mobile/engagement/trend — not verified introduced by 7.1
D7 query returned 7 entries spanning 2026-05-02 → 2026-05-08 (7 days inclusive of tomorrow), but today is 2026-05-07 ET. Window appears to be "today + 6 prior days" but the Tokyo result also shows 7 entries ending 2026-05-07 — so for some TZ values the window correctly ends today and for others it ends tomorrow.
Worth investigating to see if Phase 7.1's TZ math introduced this or whether it's a pre-existing engagement endpoint quirk. Not blocking phase approval.