Commit graph

283 commits

Author SHA1 Message Date
dfd0a9f2b2 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
2026-05-07 07:56:32 -04:00
14f4da3483 feat(07.1-04): migrate mobile finance + ticket detail to useUserTimezone
- app/mobile/finance/page.tsx: thread tz through fmtDate, setLastSync, monthLabel — 3 formatter callsites now pass timeZone
- app/mobile/tickets/[id]/page.tsx: thread tz through fmtDate (5 callsites) and TimelineCard prop
- All toLocaleDateString / toLocaleString calls in both files now render in user.timezone, not browser local zone
- Resolves TZ-02 on the directly-reported bug surface (mobile finance + ticket detail)
2026-05-07 07:54:21 -04:00
2ac2db7a23 feat(07.1-04): add useUserTimezone client hook
- New lib/hooks/use-user-timezone.ts exporting useUserTimezone() and formatInUserTimezone()
- Reads user.timezone from Better Auth useSession() additionalField (Plan 01)
- Validates against Intl.supportedValuesOf('timeZone') with safe fallback to NEXT_PUBLIC_DEFAULT_TIMEZONE || 'UTC'
- Pure formatInUserTimezone helper safe to call inside loops (not a hook)
- Resolves TZ-04
2026-05-07 07:52:16 -04:00
3a3564fc91 chore: merge 07.1-02 worktree commits 2026-05-07 07:48:54 -04:00
46ee1f6ade docs(07.1-01): complete user timezone column + Better Auth additionalField
- TZ-01 satisfied: per-user IANA timezone column added to "user" table
- session.user.timezone now exposed via Better Auth additionalFields
- Defaults: SQL DEFAULT 'UTC', app-level default reads process.env.DEFAULT_TIMEZONE
- No destructive ops; storage timezone of existing TIMESTAMP columns unchanged
2026-05-07 07:38:08 -04:00
1b80b3f7ab docs(07.1-02): complete user timezone endpoint plan
SUMMARY.md documenting GET/PUT /api/me/timezone shapes, IANA validation
rule, threat-model dispositions, and self-check results.
2026-05-07 07:37:25 -04:00
061f266b18 feat(07.1-01): expose user timezone on Better Auth session
- Adds `timezone` to additionalFields on the auth `user` config
- Default value reads process.env.DEFAULT_TIMEZONE (falls back to "UTC")
- session.user.timezone now available on every authenticated request
- Inferred User type automatically picks up the new field — no type changes needed
2026-05-07 07:36:35 -04:00
f50215f8fc feat(07.1-02): add GET/PUT /api/me/timezone endpoint
- New app/api/me/timezone/route.ts with GET + PUT handlers
- requireAuth() gate on both methods (401 unauthenticated)
- IANA whitelist via Intl.supportedValuesOf('timeZone') + 64-char cap
- PUT writes only session.user.id — no userId body/query param
- Updates audit column updated_at = NOW() on write
- Resolves TZ-03
2026-05-07 07:36:01 -04:00
25e6b7599a feat(07.1-01): add user timezone column migration
- Adds `timezone TEXT NOT NULL DEFAULT 'UTC'` to "user" table (TZ-01)
- Backfills any NULL rows defensively
- Idempotent: ADD COLUMN IF NOT EXISTS, no destructive ops
- Storage timezone of existing TIMESTAMP columns unchanged
2026-05-07 07:35:51 -04:00
bee35e0260 fix(auth): reduce mobile sign-in friction (PWA + auto-redirect)
Three independent changes that together stop the mobile re-auth churn:

- app/layout.tsx: add appleWebApp metadata so iOS "Add to Home Screen"
  launches Pulse in true standalone mode (own cookie jar, persists
  across Safari memory pressure)
- components/auth/sign-in-form.tsx: when /auth/sign-in mounts and
  ?callbackUrl starts with /mobile, auto-call authClient.signIn.social
  for Microsoft. With an active M365 browser session this redirect is
  silent — the user lands on /mobile/* with no tap.
- app/auth/sign-in/page.tsx: wrap SignInForm in <Suspense> (required
  by Next.js 16 because SignInForm now uses useSearchParams)

Pairs with operator-side env bump SESSION_TIMEOUT_SECONDS=2592000
(30 days, .env files are gitignored — applied on the running container
via docker compose up -d --force-recreate app).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:05:52 -04:00
f9ab954518 docs(phase-07.1): plan urgent user timezone fix (TZ-01..TZ-04)
Insert Phase 7.1 between Phase 7 and Phase 8 to address dashboards/filters
rendering wrong dates because day/week boundary math runs in server UTC
instead of the viewing user's timezone. Persistence stays UTC; only the
read/display path changes.

5 plans in 3 waves:
- 07.1-01 (Wave 1): migration 083 + Better Auth additionalField timezone
- 07.1-02 (Wave 1): /api/me/timezone GET+PUT with IANA validation
- 07.1-03 (Wave 2): server-side AT TIME ZONE migration across 6 routes,
  including auth-gate fix on /api/mobile/finance and trends route
- 07.1-04 (Wave 2): useUserTimezone() hook + 2 mobile pages + codebase audit
- 07.1-05 (Wave 3): codebase-wide useUserTimezone() adoption per audit

Add Phase 9 stub (User Profile & Preferences) to roadmap for the picker UI
that reuses 7.1's hook + endpoint.

REQUIREMENTS.md TZ-02 carves out engagement_snapshots UTC bucketing as a
documented exception (≤24h drift acceptable for admin overview).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:02:07 -04:00
0dec54ca4f docs(phase-07): evolve PROJECT.md after phase completion 2026-05-03 23:04:41 -04:00
a288df5b34 docs(phase-07): complete phase execution 2026-05-03 23:04:17 -04:00
349279a6ef test(07): persist human verification items as UAT 2026-05-03 23:04:12 -04:00
0af6136864 docs(07-03): complete mobile engagement page plan summary
- Documents orchestration model (3 fetches on period, 1 on sort, 0 on search)
- Documents deviation: lowercase EngagementSortKey values vs plan template (Wave 2 actual)
- Documents field mapping: existing endpoint 'email' -> component 'userEmail'
- Confirms DRAWER-03 reachability and ENG-09 BottomNav unchanged
- Flags inherited risk T-07-12 (IDOR on /api/engagement/users) for follow-up
2026-05-03 22:58:41 -04:00
5daf7f31e5 feat(07-03): create mobile engagement page (plan 01 endpoints + plan 02 components)
- New app/mobile/engagement/page.tsx ('use client', 378 lines)
- Period chips (D30 default), 3 independent fetches on mount/period change
- Sort chips (hours default), refetch users only on sort change
- Client-side search filter (useMemo, 300ms debounce via EngagementSearchInput)
- IntersectionObserver infinite scroll (rootMargin 200px) + Load more fallback
- 4 summary card skeletons + sparkline skeleton + 5 row skeletons on initial load
- Empty state (activeUsers === 0 + users.length === 0), not-configured banner, no-matches inline
- toast.error per failing fetch; Load more flips to Retry on error
- BottomNav and MoreDrawer unchanged (ENG-09 / D-01 / D-02)
2026-05-03 22:57:44 -04:00
d63789224e feat(07-02): add 7 engagement components (chips, summary card, sparkline, sort, search, user row + skeleton) 2026-05-03 22:53:33 -04:00
ccd2177977 docs(07-02): complete engagement component primitives plan summary
- 7 presentational components built: PeriodChips, SummaryCard, HoursSparkline, SortChips, SearchInput, UserRow, UserRowSkeleton
- getInitials exported from EngagementUserRow for Phase 8 reuse
- No recharts — inline SVG sparkline only (DASH-04)
- tsc exits 0
2026-05-03 22:52:04 -04:00
00d0102168 chore: merge executor worktree (07-01) 2026-05-03 22:45:36 -04:00
ecc7177832 docs(07-01): complete mobile engagement API endpoints plan summary 2026-05-03 22:45:15 -04:00
c3d370c2f0 feat(07-01): add /api/mobile/engagement/trend endpoint
- GET handler with requireAuth() gate before any DB query (T-07-01)
- Period whitelist ['D7','D30','D90'] with 400 for invalid values (T-07-02)
- generate_series ensures continuous daily series (D-15: no gaps)
- Returns EngagementTrendResponse with D7→7, D30→30, D90→90 SparklinePoints
- Bounded result set: whitelist caps to max 90 rows (T-07-03)
- Exports SparklinePoint and EngagementTrendResponse for Plan 03 import
2026-05-03 22:44:10 -04:00
f4a9fd83db feat(07-01): add /api/mobile/engagement/summary endpoint
- GET handler with requireAuth() gate before any DB query (T-07-01)
- Period whitelist ['D7','D30','D90'] with 400 for invalid values (T-07-02)
- Returns MobileEngagementSummary: configured, activeUsers, totalGraphHours, totalAutotaskHours, hoursPerActiveUser
- Reuses notAutomatedFilter and wulfconsulting email scope from desktop summary
- Exports MobileEngagementSummary interface for Plan 03 page import
2026-05-03 22:43:36 -04:00
1ec561bc29 docs(07): record planning state 2026-05-03 22:41:03 -04:00
d4841a7eb5 docs(07): create phase 7 engagement overview plans 2026-05-03 22:38:21 -04:00
71b20612b2 docs(07): UI design contract 2026-05-03 22:27:13 -04:00
8b6e8b27b1 docs(07): UI design contract for Engagement Overview 2026-05-03 22:22:41 -04:00
192eeb5bb6 docs(state): record phase 7 context session 2026-05-03 22:18:33 -04:00
c85e6347c7 docs(07): capture phase context (auto mode) 2026-05-03 22:18:28 -04:00
97a54fd8d8 docs(phase-06): evolve PROJECT.md after phase completion 2026-05-03 21:44:49 -04:00
2f56d7c808 docs(phase-06): complete phase execution 2026-05-03 21:44:21 -04:00
d26ddc1fae test(06): persist human verification items as UAT 2026-05-03 21:44:13 -04:00
cddf9cac8c docs(06-03): complete mobile analyzer detail page plan summary
- Documents title/company omission from identity block (D-25/D-36 conflict resolution)
- Documents IDOR posture T-06P03-02 as accept-and-flag with STATE.md follow-up recommendation
- Confirms no desktop files touched, TypeScript clean
2026-05-03 21:37:50 -04:00
aa4ff00065 feat(06-03): add mobile analyzer detail page /mobile/analyzer/[id]
- Real segment route reading GET /api/analyzer/analyses/[id] (D-25, reused as-is)
- Three content sections: Summary / Next Step / Next Step Rationale (D-21, ANL-03)
- Identity block: ticket# badge, completed-at relative time, stage pips, confidence badge, Review pill
- Header: back chevron (router.back()) + breadcrumb 'Analyzer / #{ticketNumber}' + external link (D-19)
- Footer: 'View full analysis' link to /analyzer/analysis/[id] with ExternalLink icon, min-h-[44px] (D-22, ANL-04)
- Read-only enforcement: zero form/edit/re-run/cancel controls (D-23, ANL-05)
- Loading skeleton, 404 state, error state with toast (D-28)
- Title/company omitted from identity block per D-25/D-36 (PersistedAnalysis lacks those fields)
2026-05-03 21:36:49 -04:00
86369bd6ab docs(06-02): complete analyzer feed UI components + page plan summary 2026-05-03 21:32:50 -04:00
c8aa69baf6 feat(06-02): replace analyzer placeholder with real feed list page
- Replaces 'coming soon' placeholder with full read-only feed
- useState/useEffect/fetch only (no SWR/react-query per CLAUDE.md D-38)
- IntersectionObserver sentinel with rootMargin 200px for auto-load
- Load more fallback button with aria-label, min-h-[44px] touch target
- 5 AnalyzerRowSkeleton instances on initial load (D-28)
- Empty state with dashed border, Sparkles icon, Open desktop Analyzer link
- toast.error on load failures; Load more flips to Retry on error
- No edit/re-run/prompt-tuning controls (ANL-05)
2026-05-03 21:31:21 -04:00
9c1a74009b feat(06-02): add AnalyzerStagePips, ConfidenceBadge, AnalyzerRowSkeleton, AnalyzerFeedRow components
- AnalyzerStagePips: 3-dot stage indicator with caret separators, sr-only accessibility label
- ConfidenceBadge: High/Medium/Low buckets (0.85/0.65) with green/amber/slate tones, dark mode
- AnalyzerRowSkeleton: Card-wrapped skeleton matching row shape (no border-l-4 per D-11)
- AnalyzerFeedRow: Full card row with header/title/summary/footer, Link to /mobile/analyzer/[id]
2026-05-03 21:30:17 -04:00
fe4fedace0 docs(06-01): complete analyzer feed API plan summary
- Document exported types, SQL approach, kiosk scoping, cursor encoding
- Include notes for Plan 06-02 executors (import path, sample URL)
- Self-check passed: file exists, commit 75238c1 verified, tsc exits 0
2026-05-03 21:25:49 -04:00
75238c12bb feat(06-01): add GET /api/mobile/analyzer/feed endpoint
- Create cursor-paginated analyzer feed endpoint for mobile
- Export AnalyzerFeedRow and AnalyzerFeedResponse types (D-26)
- Implement DISTINCT ON CTE for latest-per-ticket analysis (D-02)
- Apply kiosk_settings company scoping via getMobileCompanyFilter() (D-04)
- Cursor keyset pagination on (completed_at, id) with base64 JSON encoding (D-06)
- Server-side limit cap at 25 (D-05); LIMIT n+1 trick for hasMore detection
- Ordering: completed_at DESC NULLS LAST, id DESC (D-03)
- Manual snake_case to camelCase transform per CLAUDE.md conventions
- Payload whitelist: only 12 AnalyzerFeedRow fields; no model_traces, itglue_docs_referenced, or human_review_reasons (T-06-05)
- requireAuth() gate before any DB query (T-06-01)
2026-05-03 21:25:01 -04:00
010aadcc81 docs(06): create phase plans (3 plans, 3 waves) 2026-05-03 21:22:01 -04:00
9119846ea7 docs(06): UI design contract 2026-05-03 21:07:09 -04:00
4b2e030ca3 docs(06): UI design contract 2026-05-03 21:04:58 -04:00
a44f28d71b docs(state): record phase 6 context session 2026-05-03 21:01:10 -04:00
d6c15e0d21 docs(06): capture phase context (auto mode) 2026-05-03 21:01:06 -04:00
1979463d05 docs(phase-5): evolve PROJECT.md after phase completion 2026-05-03 20:29:11 -04:00
62668ce8df docs(phase-5): complete phase execution 2026-05-03 20:28:51 -04:00
a7759e3029 test(05): persist human verification items as UAT 2026-05-03 20:02:36 -04:00
3b582ace53 docs(05-02): complete finance page restyle plan summary
- Finance page rewritten to Phase 5 visual contract (FIN-01, FIN-02)
- 4 KpiCardMobile tiles, 2 shadcn Collapsibles, stacked revenue list
- All 23 locked decisions (D-01 through D-23) delivered
- Phase 5 complete: both plans committed and summarized
2026-05-03 19:58:15 -04:00
a9a5a987f4 feat(05-02): rewrite mobile finance page to Phase 5 visual contract
- Replace AR Hero gradient + standalone Revenue YTD with 4 KpiCardMobile tiles (2x2 grid)
- Drop bar chart; add monthly-revenue stacked list (D-09 / DASH-04 precedent)
- Add shadcn Collapsible for Open Invoices and Recent Payments (D-15, D-16)
- Aging row uses locked amber/orange/destructive palette (D-08)
- Top AR by Customer rendered as stacked list with proportion bars (D-07)
- toast.success + toast.error on sync outcomes (D-17, D-18)
- FinanceSkeleton on initial load; destructive retry card on error (D-17, D-18)
- D-19 empty state when total_ar===0 and open_invoices.length===0
- D-23: no page H1; header controls row with aria-labels (D-14)
- No font-medium, no font-bold, no raw red/yellow/green Tailwind classes
2026-05-03 19:56:36 -04:00
1ce80371d1 docs(05-01): complete FinanceRow + FinanceSkeleton plan
- FinanceRow: 2-line stacked row with amountTone variants (D-06)
- FinanceSkeleton: initial-load placeholder matching full page layout (D-17)
- Both components type-check clean, no files outside components/mobile/ modified
2026-05-03 19:53:14 -04:00
5f7fc29e67 feat(05-01): create FinanceSkeleton component
- Initial-load placeholder for /mobile/finance (D-17)
- 2x2 KPI grid (h-20) + 3-cell aging row (h-16) + 2x list-row blocks (3 rows each)
- Uses Skeleton primitive from components/ui/skeleton
- Locked spacing tokens: px-4 py-4, space-y-6, gap-3, gap-2, space-y-3
- No props, no Card wrapper, no custom animation per UI-SPEC
2026-05-03 19:52:25 -04:00