fix: Maintenance dates display one day off due to timezone conversion #237

Closed
opened 2026-03-24 01:51:36 +00:00 by egullickson · 0 comments
Owner

Problem

Maintenance record dates are displayed one day off when editing. A record listed as 3/13/2026 in the record list shows 03/14/2026 when the edit dialog opens. The bug was reproduced on staging.motovaultpro.com using the 2017 Corvette maintenance records.

Root Cause

The PostgreSQL DATE type (OID 1082) is returned by the pg driver as a JavaScript Date object created at local midnight on the server. When Fastify serializes the API response via JSON.stringify(), it calls Date.toJSON() which internally calls toISOString(), converting to UTC. The timezone offset between server local time and UTC shifts the date forward or backward by one day depending on the server's timezone.

On the frontend, dayjs() parses the UTC ISO string (e.g., 2026-03-13T05:00:00.000Z) and displays it in the user's local timezone, compounding the mismatch. Additionally, DatePicker onChange handlers use toISOString().split('T')[0] to convert selected dates back to strings, which performs the same unsafe UTC conversion when saving.

Data flow (before fix):

PostgreSQL DATE "2026-03-13"
  -> pg driver: new Date(2026, 2, 13) at server local midnight
  -> Fastify JSON: Date.toISOString() = "2026-03-13T05:00:00.000Z" (CDT offset)
  -> Frontend: dayjs("2026-03-13T05:00:00.000Z") = March 13 midnight CDT (or March 14 depending on offset direction)
  -> DatePicker displays wrong date

Solution (Full Stack, Option C from analysis)

Backend (1 file): Override pg type parser for DATE columns to return plain YYYY-MM-DD strings instead of Date objects. This is the primary architectural fix.

// database.ts
types.setTypeParser(1082, (val: string) => val);

Frontend (6 files): Replace all toISOString().split('T')[0] with dayjs.format('YYYY-MM-DD') in DatePicker onChange handlers. Parse API dates defensively with substring(0, 10) in edit dialogs.

Affected Files

File Change Severity
backend/src/core/config/database.ts Add setTypeParser(1082) CRITICAL
frontend/.../MaintenanceRecordEditDialog.tsx Defensive date parse + format CRITICAL
frontend/.../MaintenanceScheduleEditDialog.tsx Defensive date parse + format CRITICAL
frontend/.../MaintenanceRecordForm.tsx Default dates + onChange format HIGH
frontend/.../MaintenanceScheduleForm.tsx onChange format HIGH
frontend/.../useMaintenanceReceiptOcr.ts OCR date parser toISOString fix MEDIUM

Acceptance Criteria

  • Record date in list view matches date in edit dialog exactly
  • Saving a record without changes does not shift the date
  • Picking a new date in the DatePicker saves the correct date
  • OCR-extracted dates are not shifted by timezone
  • Fuel logs (TIMESTAMP, not DATE) are unaffected
  • Works regardless of server or client timezone

Analysis

Codebase analysis and problem analysis skills were run. Four alternative solutions were evaluated (backend-only, frontend-only, full-stack, repository-layer). Full-stack was selected for defense-in-depth: the backend type parser prevents the bug at the source, and frontend changes guard against regressions and fix the independent toISOString() bug in onChange handlers.

## Problem Maintenance record dates are displayed one day off when editing. A record listed as **3/13/2026** in the record list shows **03/14/2026** when the edit dialog opens. The bug was reproduced on staging.motovaultpro.com using the 2017 Corvette maintenance records. ## Root Cause The PostgreSQL `DATE` type (OID 1082) is returned by the `pg` driver as a JavaScript `Date` object created at local midnight on the server. When Fastify serializes the API response via `JSON.stringify()`, it calls `Date.toJSON()` which internally calls `toISOString()`, converting to UTC. The timezone offset between server local time and UTC shifts the date forward or backward by one day depending on the server's timezone. On the frontend, `dayjs()` parses the UTC ISO string (e.g., `2026-03-13T05:00:00.000Z`) and displays it in the user's local timezone, compounding the mismatch. Additionally, `DatePicker` onChange handlers use `toISOString().split('T')[0]` to convert selected dates back to strings, which performs the same unsafe UTC conversion when saving. **Data flow (before fix):** ``` PostgreSQL DATE "2026-03-13" -> pg driver: new Date(2026, 2, 13) at server local midnight -> Fastify JSON: Date.toISOString() = "2026-03-13T05:00:00.000Z" (CDT offset) -> Frontend: dayjs("2026-03-13T05:00:00.000Z") = March 13 midnight CDT (or March 14 depending on offset direction) -> DatePicker displays wrong date ``` ## Solution (Full Stack, Option C from analysis) **Backend (1 file):** Override pg type parser for DATE columns to return plain YYYY-MM-DD strings instead of Date objects. This is the primary architectural fix. ```typescript // database.ts types.setTypeParser(1082, (val: string) => val); ``` **Frontend (6 files):** Replace all `toISOString().split('T')[0]` with `dayjs.format('YYYY-MM-DD')` in DatePicker onChange handlers. Parse API dates defensively with `substring(0, 10)` in edit dialogs. ## Affected Files | File | Change | Severity | |------|--------|----------| | `backend/src/core/config/database.ts` | Add setTypeParser(1082) | CRITICAL | | `frontend/.../MaintenanceRecordEditDialog.tsx` | Defensive date parse + format | CRITICAL | | `frontend/.../MaintenanceScheduleEditDialog.tsx` | Defensive date parse + format | CRITICAL | | `frontend/.../MaintenanceRecordForm.tsx` | Default dates + onChange format | HIGH | | `frontend/.../MaintenanceScheduleForm.tsx` | onChange format | HIGH | | `frontend/.../useMaintenanceReceiptOcr.ts` | OCR date parser toISOString fix | MEDIUM | ## Acceptance Criteria - [ ] Record date in list view matches date in edit dialog exactly - [ ] Saving a record without changes does not shift the date - [ ] Picking a new date in the DatePicker saves the correct date - [ ] OCR-extracted dates are not shifted by timezone - [ ] Fuel logs (TIMESTAMP, not DATE) are unaffected - [ ] Works regardless of server or client timezone ## Analysis Codebase analysis and problem analysis skills were run. Four alternative solutions were evaluated (backend-only, frontend-only, full-stack, repository-layer). Full-stack was selected for defense-in-depth: the backend type parser prevents the bug at the source, and frontend changes guard against regressions and fix the independent toISOString() bug in onChange handlers.
egullickson added the
status
in-progress
type
bug
labels 2026-03-24 01:51:46 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: egullickson/motovaultpro#237