- 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
15 KiB
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 formattersapp/admin/sync/duo/page.tsx:281—value.toLocaleString()app/admin/sync/veeam/page.tsx:162,177— record countsapp/admin/sync/itglue/page.tsx:116-125,150,194— stat countsapp/admin/sync/mimecast/page.tsx:35,866,901,903,905— count formattersapp/admin/sync/sentinelone/page.tsx:100,123,137,165— record countsapp/admin/data-browser/ticket-notes/page.tsx:180,181— total countsapp/admin/data-browser/time-entries/page.tsx:494,496— total countsapp/admin/qbo/page.tsx:43—n.toLocaleString()helperapp/analyzer/tickets/page.tsx:744—total.toLocaleString()app/dashboard/page.tsx:426,427— stat countsapp/sentinelone/coverage/page.tsx:96—Number(value).toLocaleString()components/admin/SyncDashboard.tsx:126,132,138— record countscomponents/admin/EntitySyncProgress.tsx:219,239— total recordscomponents/admin/ChunkedSyncProgress.tsx:117,131,166— chunk countscomponents/analyzer/analysis-view.tsx:118,119— token countscomponents/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_modifiedis the unique set of leak file paths above (39 files). - Plan 05 must:
- Add
useUserTimezone()(or its server equivalent for non-client files) to each of the 39 files. - Thread
tzinto every leak callsite listed above so the count oftimeZone:≥ count oftoLocaleDateString(+toLocaleString(+toLocaleTimeString(callsites in each file. - Skip the four file groups documented as out-of-scope (Number-format / Server-side / Deliberate-UTC / already-migrated).
- Verify with the same positive-assertion grep approach Plan 04 used.
- Add
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.