From dfd0a9f2b2907b6a8d910ca99554bf2f70d97364 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 7 May 2026 07:56:32 -0400 Subject: [PATCH] docs(07.1-04): codebase-wide tz audit + Plan 05 dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../07.1-04-AUDIT.md | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-04-AUDIT.md diff --git a/.planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-04-AUDIT.md b/.planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-04-AUDIT.md new file mode 100644 index 0000000..0c29971 --- /dev/null +++ b/.planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-04-AUDIT.md @@ -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.