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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
DATEtype (OID 1082) is returned by thepgdriver as a JavaScriptDateobject created at local midnight on the server. When Fastify serializes the API response viaJSON.stringify(), it callsDate.toJSON()which internally callstoISOString(), 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,DatePickeronChange handlers usetoISOString().split('T')[0]to convert selected dates back to strings, which performs the same unsafe UTC conversion when saving.Data flow (before fix):
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.
Frontend (6 files): Replace all
toISOString().split('T')[0]withdayjs.format('YYYY-MM-DD')in DatePicker onChange handlers. Parse API dates defensively withsubstring(0, 10)in edit dialogs.Affected Files
backend/src/core/config/database.tsfrontend/.../MaintenanceRecordEditDialog.tsxfrontend/.../MaintenanceScheduleEditDialog.tsxfrontend/.../MaintenanceRecordForm.tsxfrontend/.../MaintenanceScheduleForm.tsxfrontend/.../useMaintenanceReceiptOcr.tsAcceptance Criteria
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.