- Add 07.1-05-MANIFEST.md with per-file migration plan for 51 leak files derived from 07.1-04-AUDIT.md (81 leak callsites total) - Each file gets pre-migration leak count + per-callsite before/after snippets + post-migration acceptance grep - Defer components/configuration-items/auvik-tab.tsx (no 'use client' — preserves layering boundary) - Defer app/admin/data-browser/time-entries/page.tsx.backup (orphaned; marked for git rm in Task 2) - Update Plan 05 files_modified frontmatter to enumerate every file Task 2 will touch (50 active migrations + 1 deletion + manifest)
23 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | requirements_addressed | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07.1-user-timezone-fix-inserted-urgent | 05 | execute | 3 |
|
|
true |
|
|
|
Purpose: Resolve the codebase-scale portion of TZ-04 + TZ-02 (client side). Plans 03 + 04 together cover the read paths and the hook itself; this plan finishes the migration work the audit revealed (>10 known leak callsites in admin/analyzer/engagement/dashboard pages and shared components).
Output: A 07.1-05-MANIFEST.md derived from the audit, plus edits to every
file the audit classified as a leak. The manifest is the source of truth for
files_modified (Plan 04's audit is what populates it) — at planning time
the exact list isn't known; the executor MUST consume the audit and update
this plan's files_modified array as the first action.
Conditional execution: If 07.1-04-AUDIT.md "Plan 05 dispatch" reports
"Plan 05 is unnecessary. Mark in SUMMARY." (Leak count == 0), skip every task
and return CLEAN immediately. Otherwise proceed.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/STATE.md @.planning/ROADMAP.md @.planning/REQUIREMENTS.md @CLAUDE.md @.planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-04-AUDIT.md @lib/hooks/use-user-timezone.ts This plan depends on Plan 04 — the hook (`@/lib/hooks/use-user-timezone`) and the audit file MUST exist before Plan 05 starts. The audit is the SINGLE source of truth for which files are migrated.The migration recipe for every leak callsite is:
-
If the file is not already
'use client', the migration is impossible (server components can't calluseUserTimezone). Re-classify the callsite as 'server_side' in a follow-up audit. (Audit step at planning time already filtered out server route files.) -
Add at the top of the imports (next to other
@/libimports):import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; -
Inside the default-exported component (or the hook entry point in a custom component), call:
const tz = useUserTimezone(); -
For each leaking call:
new Date(ts).toLocaleString('en-US', { month: 'short', ... })→
new Date(ts).toLocaleString('en-US', { month: 'short', ..., timeZone: tz })For module-scope helper functions (
function fmt(d) { return new Date(d).toLocaleString() }):- Convert to accept
tz: stringas a new parameter:function fmt(d, tz) { return new Date(d).toLocaleString(undefined, { timeZone: tz }) } - Update every callsite of that helper in the same file to pass
tz.
- Convert to accept
-
For DataTable column
render: (value) => new Date(value).toLocaleDateString()patterns (common in admin pages), the migration is to:- Move the column definitions inside the component, OR
- Pass
tzvia a closure when the columns are constructed inside the component, OR - Use the
formatInUserTimezonehelper from@/lib/hooks/use-user-timezoneif column definitions stay at module scope andtzcan be threaded as a param to a column-builder function.
-
Re-run the codebase-wide grep AFTER all migrations:
grep -rEn "Intl\\.DateTimeFormat|\\.toLocaleDateString\\(|\\.toLocaleTimeString\\(|\\.toLocaleString\\(" \\ app/ components/ lib/hooks/The post-migration result must contain ONLY:
timeZone:in the same call (migrated → satisfied)- server-side route handlers under
app/api/**/route.ts(deliberately server, out of scope) components/ui/calendar.tsx(shadcn primitive — calendar-cell labels, not user-visible dates)- Number formatters (
.toLocaleString()onnumber/bigint— not date formatters) - Files in the audit's "deliberate_utc" classification
Browser environment variable:
process.env.NEXT_PUBLIC_DEFAULT_TIMEZONEis the client-readable equivalent ofDEFAULT_TIMEZONE. Same fallback semantics asuseUserTimezone().
Group the leak callsites by file. For each file, produce a section:
### <file path>
- 'use client' status: <yes|no — needs `'use client'` added or refactor>
- Pre-migration leak count: <N>
- Post-migration acceptance grep:
[ "$(grep -c 'timeZone:' <file>)" -ge <N> ]
- Per-callsite plan:
- Line <L>: `<snippet before>` → `<snippet after>`
- ...
Notes / risks: <any per-file gotchas — e.g., DataTable column factory
at module scope, helper that needs `tz` parameter threading>
Then write a `## Files to migrate` summary list at the top with `[ ]`
checkboxes — Task 2 ticks them off as it migrates each file.
After writing the manifest, update Plan 05's `files_modified` frontmatter
array to include EVERY file path enumerated in `## Files to migrate`,
PLUS the manifest path itself. (Note: this requires editing
`07.1-05-PLAN.md` in place. Use the `Edit` tool to update only the
`files_modified:` block.)
Notes:
- Do NOT skip module-scope helpers — converting them to accept `tz` as a
parameter is part of the migration. The audit may flag these as a
"Notes" gotcha; the manifest captures the exact transformation.
- Files where the migration would require >5 component-shape changes
(e.g., refactoring a class component to functional, or moving a large
module-scope formatter into the component) should be flagged as
"DEFER — out of scope for Plan 05" with a brief rationale and added to
a `## Deferred` list at the bottom of the manifest. The deferred set
becomes a v2 follow-up (a future phase).
test -f .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-05-MANIFEST.md && grep -q '## Files to migrate' .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-05-MANIFEST.md
- File exists at `.planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-05-MANIFEST.md`
- Either: contains `Leak count: 0 — Plan 05 skipped` (and Tasks 2+3 are skipped), OR
- Contains a `## Files to migrate` checklist with one entry per leak file from the audit
- Each leak file has a `Per-callsite plan:` block enumerating every leak callsite by line number with before/after snippets
- Plan 05's `files_modified` frontmatter has been updated to include every file in `## Files to migrate` plus the manifest path
- Any file deferred is listed under `## Deferred` with rationale
The manifest is the single source of truth for what Task 2 migrates and
what Task 3's verification grep checks. Plan 05's `files_modified`
accurately reflects every file this plan will touch.
Task 2: Migrate every leak callsite per the manifest
(see 07.1-05-MANIFEST.md `## Files to migrate` — populated by Task 1)
- .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-05-MANIFEST.md (Task 1's output — the per-file migration plan)
- lib/hooks/use-user-timezone.ts (the import target)
- app/mobile/finance/page.tsx, app/mobile/tickets/[id]/page.tsx (Plan 04's reference migrations — copy the call shape)
For each file in the manifest's `## Files to migrate` checklist, in the
order they appear:
1. Open the file.
2. If it does not already declare `'use client'` at the top, STOP and
move it to `## Deferred` in the manifest with rationale "would require
'use client' conversion or component refactor — out of scope for Plan
05". Do NOT add `'use client'` to a file that doesn't have it — that's
a non-trivial change in Pulse (server components are fine for static
shells; the user explicitly chose this layering).
3. Add `import { useUserTimezone } from '@/lib/hooks/use-user-timezone';`
next to the other `@/lib` imports.
4. Inside the component (or each component if the file exports multiple),
add `const tz = useUserTimezone();` near the top of the function body
(before any useState/useEffect calls).
5. Apply each per-callsite transformation from the manifest verbatim.
6. For module-scope helpers, convert to accept `tz: string` as a new
parameter and update every callsite in the same file.
7. Tick off the file in the manifest's `## Files to migrate` checklist.
8. Verify the file with the per-file acceptance grep recorded in the
manifest:
[ "$(grep -c 'timeZone:' <file>)" -ge <pre-migration leak count> ]
Do NOT modify files outside the manifest's `## Files to migrate` list.
Do NOT modify files in `## Deferred`.
Type-check after each file (or at the end of the batch) to catch any
parameter-threading regressions:
ERR=$(npx tsc --noEmit --pretty 2>&1 | grep -E "<file_path>"); [ -z "$ERR" ]
Notes:
- The DataTable column-render pattern (common in `app/admin/data-browser/*/page.tsx`)
may need a column-builder function that takes `tz` as a closure variable.
The manifest will have called this out per file. Do NOT introduce a
`useMemo` for column definitions unless the file already uses one
(premature optimization).
- Some helper functions (e.g., `relTime`, `formatDate`, `fmt`) are defined
at module scope in many files. Threading `tz` as an extra parameter is
intentional — no `React.useContext` workaround.
- The shared admin sync formatter (e.g., `app/admin/sync/datto-rmm/page.tsx:23`)
is a candidate for moving inside the component OR threading `tz`. The
lighter-touch fix is threading.
MANIFEST=.planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-05-MANIFEST.md; if grep -q "Plan 05 skipped" "$MANIFEST"; then echo "skipped"; else FILES=$(grep -E "^- \[x\] " "$MANIFEST" | sed -E 's/^- \[x\] //' | tr '\n' ' '); ALL_OK=1; for f in $FILES; do if ! grep -q "useUserTimezone" "$f"; then echo "MISSING: $f"; ALL_OK=0; fi; done; [ "$ALL_OK" = "1" ]; fi
- Either: manifest reports "Plan 05 skipped" (Task 2 is a no-op) — accepted, OR
- Every file in the manifest's `## Files to migrate` checklist is checked off `[x]`
- Every checked-off file imports `useUserTimezone` from `@/lib/hooks/use-user-timezone`
- Every checked-off file calls `useUserTimezone()` inside the component
- Every per-file acceptance grep in the manifest passes
- `ERR=$(npx tsc --noEmit --pretty 2>&1 | grep -E ""); [ -z "$ERR" ]` for every migrated file
Every leak file in the audit has been migrated to consume
`useUserTimezone()`. Deferred files (refactor-blocking) are documented
explicitly. TypeScript compiles for every migrated file.
Task 3: Codebase-wide post-migration verification grep
(read-only verification; no file edits)
- .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-05-MANIFEST.md (the deferred list — informs the expected residue)
- .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-04-AUDIT.md (the deliberate_utc + server_side classifications — informs the expected residue)
Re-run the codebase-wide leak discovery grep AFTER Task 2 finishes:
grep -rEn "Intl\\.DateTimeFormat|\\.toLocaleDateString\\(|\\.toLocaleTimeString\\(" \\
app/ components/ lib/hooks/ \\
| grep -v "node_modules" \\
| grep -v "components/ui/calendar.tsx" \\
> /tmp/tz-post-migration.txt
For each line in the output, classify:
- Has `timeZone:` in the same call → migrated ✓
- Lives under `app/api/**/route.ts` → server-side, out of scope ✓
- Listed in audit's `deliberate_utc` section → out of scope ✓
- Listed in manifest's `## Deferred` section → known follow-up ✓
- None of the above → REGRESSION. Stop and re-migrate.
Acceptable residue:
- Server-side route handlers (Plan 03's responsibility, but they don't
consume the client hook anyway)
- Deliberate UTC pins
- Deferred files (count must match the manifest's `## Deferred` count)
Document the residue list in `07.1-05-SUMMARY.md` for posterity.
Also run a final TypeScript check across all modified files:
FILES=$(grep -E "^- \\[x\\] " .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-05-MANIFEST.md | sed -E 's/^- \\[x\\] //')
for f in $FILES; do
ERR=$(npx tsc --noEmit --pretty 2>&1 | grep -E "$f")
[ -z "$ERR" ] || { echo "TS errors in $f"; exit 1; }
done
RESIDUE=$(grep -rEn "Intl\.DateTimeFormat|\.toLocaleDateString\(|\.toLocaleTimeString\(" app/ components/ lib/hooks/ 2>/dev/null | grep -v node_modules | grep -v "components/ui/calendar.tsx" | grep -v "timeZone:" | grep -v "/route.ts:" | wc -l); MANIFEST=.planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-05-MANIFEST.md; DEFERRED=$(grep -cE "^- " "$MANIFEST" 2>/dev/null | head -1); DELIBERATE=$(grep -cE "^\| " .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-04-AUDIT.md 2>/dev/null); echo "post-migration residue lines: $RESIDUE — must equal deferred + deliberate-UTC count from audit/manifest"; [ "$RESIDUE" -le "$((DEFERRED + DELIBERATE))" ]
- Post-migration residue grep returns ≤ (deferred files in manifest + deliberate-UTC files in audit)
- Every residue line is accounted for by either: deferred classification, deliberate-UTC classification, or `timeZone:` already present
- No new "leak" callsite exists that wasn't classified by the audit OR migrated by Task 2
- All migrated files pass `npx tsc --noEmit --pretty` filtered to that file
- `07.1-05-SUMMARY.md` documents the residue list and any deferred-file rationale
The codebase-wide grep proves SC#4 is satisfied: every leak callsite is
either migrated, deferred (with rationale), or out-of-scope (server-side
or deliberate UTC). No new leak surfaces have been introduced.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Server → client (session payload) | Same boundary as Plan 04 — tz string travels via Better Auth session cookie; client trusts it for formatting only |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-07.1-05-01 | Tampering | Tampered session payload with malformed tz | mitigate | Same as Plan 04: useUserTimezone() validates against Intl.supportedValuesOf('timeZone') and falls back to `NEXT_PUBLIC_DEFAULT_TIMEZONE |
| T-07.1-05-02 | Information Disclosure | tz exposed in client memory across more pages | accept | Same as Plan 04: tz is non-sensitive metadata. The change here just propagates the same exposure to additional pages — not a new threat. |
| T-07.1-05-03 | Tampering | A migration accidentally drops or reformats the date | mitigate | Per-file acceptance grep in the manifest verifies that the count of timeZone: ≥ the pre-migration leak count. If a migration accidentally drops a timeZone: thread, the grep catches it. TypeScript also catches missing-arg regressions where module-scope helpers gained a tz parameter. |
| T-07.1-05-04 | Repudiation | Inconsistent date display between users with different tz preferences | accept | Intentional — this is the whole point of the phase. Two users in different tzs SHOULD see different "today" buckets. |
| T-07.1-05-05 | Elevation of Privilege | Component rendering server-only data with client-only hook | mitigate | Task 2's 'use client' precondition: any file lacking the directive is moved to deferred. We do NOT silently add 'use client' to a server component (would change rendering semantics). |
| </threat_model> |
- Static (precondition):
test -f .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-04-AUDIT.md - Static (manifest exists):
test -f .planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-05-MANIFEST.md - Static (skip path): if manifest contains
Plan 05 skipped, plan returns CLEAN with no other checks. - Static (migration coverage): every file in manifest's
## Files to migratechecklist is checked-off and containsuseUserTimezone. - Static (post-migration residue): see Task 3 verify.
- Type:
ERR=$(npx tsc --noEmit --pretty 2>&1 | grep -E "<each modified file>"); [ -z "$ERR" ]for every migrated file. - Runtime (spot check, with the dev server running and a logged-in user with
timezone='America/New_York'): open one or two of the most-trafficked migrated pages (e.g.,/admin/audit/audit-log-tableconsumer,/analyzer/queue,/dashboard); verify date strings respect the user-tz (set device tz to UTC, confirm rendered strings match ET).
<success_criteria>
- Phase 7.1 SC#4 satisfied at codebase scale: no client-component leak callsites remain (only server-side, deliberate-UTC, and explicitly-deferred residues)
- Every migrated file imports
useUserTimezoneand threadstimeZone:into every formatter call - Module-scope helpers that previously had no tz access now accept
tz: stringas a parameter - TypeScript compiles for every migrated file
- Deferred-file rationale is documented for any file the manifest excluded </success_criteria>