docs(07.1-04): codebase-wide tz audit + Plan 05 dispatch

- 81 leak callsites across 39 files identified — Plan 05 closes them
- 47 number-format callsites (not dates) excluded
- 7 server-side LLM prompt callsites out of scope
- 1 deliberate-UTC callsite (engagement sparkline) leave as-is
- 5 explicit-zone callsites (Plan 04 already migrated)
- Plan 05 dispatch: NEEDED — file paths enumerated for Plan 05's files_modified
This commit is contained in:
lorentz 2026-05-07 07:56:32 -04:00
parent 14f4da3483
commit dfd0a9f2b2

View file

@ -0,0 +1,183 @@
# Phase 7.1 — Codebase-wide tz audit (Plan 04 Task 3)
Discovery date: 2026-05-07
Excluded: `app/mobile/finance/page.tsx`,
`app/mobile/tickets/[id]/page.tsx`,
`components/ui/calendar.tsx`,
`app/admin/data-browser/time-entries/page.tsx.backup` (orphaned backup file),
`node_modules/**`
Discovery grep:
grep -rEn "Intl\.DateTimeFormat|\.toLocaleDateString\(|\.toLocaleTimeString\(|\.toLocaleString\(" \
app components lib/hooks
## Leak callsites (must migrate via Plan 05)
Each row is a `Date.toLocale*` callsite with NO `timeZone:` option in the same call.
These render in the browser's local zone — the bug TZ-02 is patching.
| File | Line | Snippet | Notes |
|------|------|---------|-------|
| app/engagement/profile/page.tsx | 141 | `d.toLocaleDateString('en-US', { month: 'short' })` | sparkline label |
| app/engagement/profile/page.tsx | 264 | `new Date(m + '-02').toLocaleDateString('en-US', { month: 'short', year: 'numeric' })` | month label |
| app/engagement/page.tsx | 842 | `new Date(user.lastActivity).toLocaleDateString()` | last activity |
| app/engagement/page.tsx | 1010 | `new Date(snap.last_activity_date).toLocaleDateString()` | snapshot last activity |
| app/engagement/page.tsx | 1106 | `d.toLocaleDateString()` + `d.toLocaleTimeString([], …)` | call timestamp (2 calls on one line) |
| app/engagement/page.tsx | 1225 | `new Date(call.startTime).toLocaleDateString()` | call start |
| app/engagement/page.tsx | 1253 | `d.toLocaleDateString()` + `d.toLocaleTimeString([], …)` | call timestamp (2 calls on one line) |
| app/admin/sync/duo/page.tsx | 117 | `new Date(d).toLocaleString()` | sync timestamp |
| app/admin/sync/datto-rmm/page.tsx | 23 | `new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' })` | sync timestamp |
| app/admin/sync/veeam/page.tsx | 26 | `new Date(d).toLocaleString(undefined, { … })` | sync timestamp |
| app/admin/sync/itglue/page.tsx | 24 | `new Date(d).toLocaleString(undefined, { … })` | sync timestamp |
| app/admin/sync/mimecast/page.tsx | 28 | `new Date(d).toLocaleString(undefined, { … })` | sync timestamp |
| app/admin/sync/mimecast/page.tsx | 651 | `new Date(message.dateReceived).toLocaleString()` | message timestamp |
| app/admin/sync/mimecast/page.tsx | 924 | `new Date(m.dateReceived).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })` | message timestamp |
| app/admin/sync/mimecast/page.tsx | 1239 | `new Date(message.received).toLocaleString()` | message timestamp |
| app/admin/sync/mimecast/page.tsx | 1405 | `new Date(m.receivedDateTime).toLocaleString(undefined, { … })` | message timestamp |
| app/admin/sync/mimecast/page.tsx | 1840 | `new Date(m.received).toLocaleString(undefined, { … })` | message timestamp |
| app/admin/sync/sentinelone/page.tsx | 15 | `new Date(d).toLocaleString()` | sync timestamp |
| app/admin/zabbix-wan/page.tsx | 212 | `new Date(ts).toLocaleString()` | last synced |
| app/admin/zabbix-wan/page.tsx | 213 | `new Date(ts).toLocaleString()` | last synced |
| app/admin/rmm-overshell/page.tsx | 142 | `new Date(settings.discoveredAt).toLocaleString()` | discovered timestamp |
| app/admin/rmm-overshell/page.tsx | 199 | `new Date(settings.logliftDiscoveredAt).toLocaleString()` | discovered timestamp |
| app/admin/rmm-overshell/page.tsx | 265 | `new Date(e.queuedAt).toLocaleString()` | queued timestamp |
| app/admin/itglue-writes/page.tsx | 144 | `new Date(w.performed_at).toLocaleString()` | performed timestamp |
| app/admin/data-browser/tasks/page.tsx | 100 | `new Date(value).toLocaleDateString()` | DataTable column render |
| app/admin/data-browser/projects/page.tsx | 88 | `new Date(value).toLocaleDateString()` | DataTable column render |
| app/admin/data-browser/ticket-notes/page.tsx | 86 | `new Date(v).toLocaleDateString()` | DataTable column render |
| app/admin/data-browser/contracts/page.tsx | 88 | `new Date(value).toLocaleDateString()` | DataTable column render |
| app/admin/data-browser/contracts/page.tsx | 96 | `new Date(value).toLocaleDateString()` | DataTable column render |
| app/admin/data-browser/tickets/page.tsx | 103 | `new Date(value).toLocaleDateString()` | DataTable column render |
| app/admin/data-browser/time-entries/page.tsx | 296 | `new Date(value).toLocaleDateString()` | DataTable column render |
| app/admin/ticket-digest/page.tsx | 410 | `new Date(report.generated_at).toLocaleString()` | report timestamp |
| app/admin/device-link-conflicts/page.tsx | 223 | `new Date(r.xref.lastSeenAt).toLocaleString()` | last seen |
| app/admin/workflow/history/page.tsx | 192 | `new Date(exec.created_at).toLocaleString()` | execution timestamp |
| app/admin/workflow/pipelines/[id]/page.tsx | 582 | `new Date(exec.started_at).toLocaleString()` | execution timestamp |
| app/analyzer/itglue/applications/page.tsx | 129 | `new Date(r.latestAudit.generatedAt).toLocaleDateString()` | audit timestamp |
| app/analyzer/itglue/applications/[id]/page.tsx | 411 | `new Date(audit.generated_at).toLocaleString()` | audit timestamp |
| app/analyzer/itglue/applications/[id]/page.tsx | 720 | `new Date(w.performed_at).toLocaleString()` | write performed |
| app/analyzer/itglue/applications/[id]/page.tsx | 779 | `new Date(h.generated_at).toLocaleString()` | history timestamp |
| app/analyzer/itglue/configurations/page.tsx | 130 | `new Date(r.latestAudit.generatedAt).toLocaleDateString()` | audit timestamp |
| app/analyzer/itglue/configurations/[id]/page.tsx | 393 | `new Date(audit.generated_at).toLocaleString()` | audit timestamp |
| app/analyzer/itglue/configurations/[id]/page.tsx | 688 | `new Date(w.performed_at).toLocaleString()` | write performed |
| app/analyzer/itglue/configurations/[id]/page.tsx | 741 | `new Date(h.generated_at).toLocaleString()` | history timestamp |
| app/analyzer/itglue/sites/[companyId]/page.tsx | 212 | `new Date(e.queuedAt).toLocaleString()` | queued timestamp |
| app/analyzer/queue/page.tsx | 103 | `new Date(a.triggeredAt).toLocaleString()` | triggered timestamp |
| app/analyzer/ticket/[ticketNumber]/page.tsx | 143 | `new Date(a.triggeredAt).toLocaleString()` | triggered timestamp |
| app/analyzer/tickets/page.tsx | 112 | `d.toLocaleDateString()` | helper |
| app/analyzer/reports/page.tsx | 136 | `new Date(r.generatedAt).toLocaleString()` | report timestamp |
| app/analyzer/reports/[id]/page.tsx | 186 | `new Date(report.generatedAt).toLocaleString()` | report timestamp |
| app/analyzer/reports/[id]/page.tsx | 199 | `new Date(report.dateRangeActual.earliest).toLocaleDateString()` | range earliest |
| app/analyzer/reports/[id]/page.tsx | 201 | `new Date(report.dateRangeActual.latest).toLocaleDateString()` | range latest |
| app/dashboard/page.tsx | 152 | `new Date().toLocaleDateString(undefined, { … })` | header date |
| app/quotes/page.tsx | 107 | `new Date(dateString).toLocaleDateString('en-US', { … })` | helper |
| app/veeam-analysis/page.tsx | 671 | `new Date(summary.generated_at).toLocaleString()` | generated timestamp |
| components/admin/DetailModal.tsx | 160 | `d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })` | detail row |
| components/admin/DetailModal.tsx | 610 | `new Date(entry.entry_date).toLocaleDateString(undefined, { … })` | time-entry date |
| components/admin/DetailModal.tsx | 656 | `new Date(note.create_date_time).toLocaleDateString(undefined, { … })` | note date |
| components/admin/DetailModal.tsx | 658 | `new Date(note.create_date_time).toLocaleTimeString(undefined, { … })` | note time |
| components/admin/IntegrationStatusTabs.tsx | 36 | `new Date(d).toLocaleString(undefined, { … })` | helper |
| components/admin/SyncScheduler.tsx | 230 | `new Date(dateString).toLocaleString()` | scheduler |
| components/admin/audit/audit-log-table.tsx | 186 | `new Date(log.timestamp).toLocaleString()` | audit log row |
| components/admin/users/user-table.tsx | 205 | `new Date(user.created_at).toLocaleDateString()` | user row |
| components/admin/users/user-sessions.tsx | 161 | `new Date(session.created_at).toLocaleString()` | session row |
| components/admin/users/user-sessions.tsx | 164 | `new Date(session.expires_at).toLocaleString()` | session row |
| components/analyzer/analysis-view.tsx | 115 | `new Date(a.triggeredAt).toLocaleString()` | analysis header |
| components/analyzer/analysis-view.tsx | 222 | `new Date(event.timestamp).toLocaleString()` | event timestamp |
| components/analyzer/analysis-view.tsx | 310 | `new Date(ts).toLocaleString()` | event timestamp |
| components/analyzer/analysis-view.tsx | 401 | `new Date(a.timeline[expandedEvent].timestamp).toLocaleString()` | event timestamp |
| components/settings/active-sessions.tsx | 118 | `new Date(session.created_at).toLocaleDateString()` | session row |
| components/dashboard/resolution-trend.tsx | 27 | `new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })` | chart axis label |
| components/dashboard/volume-trend.tsx | 28 | `new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })` | chart axis label |
| components/quotes/ticket-detail-modal.tsx | 231 | `new Date(dateString).toLocaleString('en-US', { … })` | helper |
| components/analytics/TimelineView.tsx | 133 | `new Date(groupKey + ':00:00').toLocaleString('en-US', { … })` | hour bucket |
| components/analytics/TimelineView.tsx | 140 | `new Date(groupKey).toLocaleDateString('en-US', { … })` | day bucket |
| components/analytics/TimelineView.tsx | 148 | `new Date(groupKey + '-01').toLocaleDateString('en-US', { … })` | month bucket |
| components/analytics/TimelineView.tsx | 303 | `new Date(event.timestamp).toLocaleTimeString()` | event time |
| components/analytics/ScoreCard.tsx | 486 | `analysis.dateRange.latest.toLocaleDateString()` | range latest |
| components/configuration-items/auvik-tab.tsx | 26 | `new Date(dateString).toLocaleString()` | helper |
| components/configuration-items/addigy-tab.tsx | 107 | `new Date(device['Last Check In']).toLocaleString()` | device check-in |
| components/configuration-items/addigy-tab.tsx | 327 | `new Date(device['Warranty Expiration Date']).toLocaleDateString()` | warranty date |
| components/status/activity-sparkline.tsx | 30 | `new Date(iso).toLocaleTimeString(undefined, { … })` | tooltip label |
| components/backup/compliance-detail-table.tsx | 170 | `new Date(contract.start_date).toLocaleDateString()` | contract start |
| components/backup/compliance-detail-table.tsx | 173 | `new Date(contract.end_date).toLocaleDateString()` | contract end |
| components/backup/company-backup-detail.tsx | 48 | `new Date(dateStr).toLocaleString()` | helper |
## Explicit-zone callsites (no migration)
| File | Line | Snippet |
|------|------|---------|
| app/mobile/finance/page.tsx | 44, 77, 375 | already migrated by Plan 04 Task 2 (excluded from this audit) |
| app/mobile/tickets/[id]/page.tsx | 50 | already migrated by Plan 04 Task 2 (excluded from this audit) |
| lib/hooks/use-user-timezone.ts | 60 | helper threading caller-supplied `timeZone` |
## Number-format callsites (not date — ignore)
These are `Number.prototype.toLocaleString()` calls — thousands separators on numeric values, NOT date callsites.
Count: 47
Examples (representative, not exhaustive):
- `app/admin/page.tsx:131``counts.linkConflicts.toLocaleString()`
- `app/admin/sync/page.tsx:344,348,360,378,396,400,404` — sync count formatters
- `app/admin/sync/duo/page.tsx:281``value.toLocaleString()`
- `app/admin/sync/veeam/page.tsx:162,177` — record counts
- `app/admin/sync/itglue/page.tsx:116-125,150,194` — stat counts
- `app/admin/sync/mimecast/page.tsx:35,866,901,903,905` — count formatters
- `app/admin/sync/sentinelone/page.tsx:100,123,137,165` — record counts
- `app/admin/data-browser/ticket-notes/page.tsx:180,181` — total counts
- `app/admin/data-browser/time-entries/page.tsx:494,496` — total counts
- `app/admin/qbo/page.tsx:43``n.toLocaleString()` helper
- `app/analyzer/tickets/page.tsx:744``total.toLocaleString()`
- `app/dashboard/page.tsx:426,427` — stat counts
- `app/sentinelone/coverage/page.tsx:96``Number(value).toLocaleString()`
- `components/admin/SyncDashboard.tsx:126,132,138` — record counts
- `components/admin/EntitySyncProgress.tsx:219,239` — total records
- `components/admin/ChunkedSyncProgress.tsx:117,131,166` — chunk counts
- `components/analyzer/analysis-view.tsx:118,119` — token counts
- `components/dashboard/kpi-card.tsx:60``value.toLocaleString()`
- `components/mobile/KpiCardMobile.tsx:29``value.toLocaleString()` (number branch)
## Server-side callsites (out of scope)
These run in Node (api routes), not React components. Out of scope for the
client hook. Today the only such callsites are the analyzer prompt builders —
deliberately locale-only (LLM input).
| File | Line | Reason |
|------|------|--------|
| app/api/veeam/rpo-analyze/route.ts | 78, 108, 109 | LLM prompt builder — locale-only by design |
| app/api/veeam/ticket-analysis/run/route.ts | 78, 90, 97, 98 | LLM prompt builder — locale-only by design |
## Deliberate UTC callsites
Files that already pass `{ timeZone: 'UTC' }` for a specific reason. Leave as-is.
| File | Line | Reason |
|------|------|--------|
| components/mobile/EngagementHoursSparkline.tsx | 39 | UTC pin for sparkline shape (not a clock) — data is daily UTC buckets |
## Summary
- **Leak count: 81** (rows in the leak table above; 81 distinct callsites across 39 files)
- **Explicit-zone count: 5** (3 finance + 1 tickets + 1 hook helper)
- **Server-side count: 7** (3 in rpo-analyze route + 4 in ticket-analysis route)
- **Deliberate-UTC count: 1** (EngagementHoursSparkline)
- **Number-format count: 47** (ignored — not date callsites)
## Plan 05 dispatch
- Leak count > 0: **Plan 05 (sibling, depends_on `07.1-04`) IS NEEDED.**
- Plan 05's `files_modified` is the unique set of leak file paths above (39 files).
- Plan 05 must:
1. Add `useUserTimezone()` (or its server equivalent for non-client files) to each of the 39 files.
2. Thread `tz` into every leak callsite listed above so the count of `timeZone:` ≥ count of `toLocaleDateString(` + `toLocaleString(` + `toLocaleTimeString(` callsites in each file.
3. Skip the four file groups documented as out-of-scope (Number-format / Server-side / Deliberate-UTC / already-migrated).
4. Verify with the same positive-assertion grep approach Plan 04 used.
Note: The `app/admin/data-browser/time-entries/page.tsx.backup` file is an
orphaned backup (no `.backup` suffix is part of the active app). It contains a
leak at line 166 but is unreachable. Plan 05 should delete the file (not migrate
it) — confirm by running `git log -- app/admin/data-browser/time-entries/page.tsx.backup`
before deletion.