Commit graph

964 commits

Author SHA1 Message Date
b1a6e6a3c3 docs(08-01): complete photo proxy plan — SUMMARY
- MsGraphClient.getUserPhotoBytes() added (3f6b135)
- /api/mobile/engagement/user/[userId]/photo route added (4978780)
- Type-check and build both pass
- All threat mitigations verified (T-08-01 through T-08-08)
2026-05-07 20:42:04 -04:00
4978780962 feat(08-01): add /api/mobile/engagement/user/[userId]/photo proxy route
- Proxies Microsoft Graph user photo bytes to authenticated mobile clients
- requireAuth() is first call — unauthenticated requests get 401 before Graph
- 503 when MSGRAPH_* env not configured (isMsgraphConfigured gate, D-26)
- 400 for malformed userId (path traversal denylist, permissive per VARCHAR(255))
- 404 neutral response when user has no photo (no userId oracle)
- 200 with Cache-Control: private, max-age=3600 on success (D-25)
- 502 neutral response on Graph upstream errors (no token/user leakage)
2026-05-07 20:41:07 -04:00
3f6b13572e feat(08-01): add getUserPhotoBytes() to MsGraphClient
- New public method fetches binary photo from Graph /users/{id}/photo/$value
- Returns { bytes, contentType } on 200, null on 404 (no photo)
- Throws on other non-2xx for upstream caller to map to 502/503
- Reuses getToken() OAuth2 cache; no retry (best-effort per D-26)
- Existing methods (getToken, fetchJson, getUsers, etc.) untouched
2026-05-07 20:39:42 -04:00
3f35e1e785 docs(08): plan Phase 8 — engagement user profile (2 plans) 2026-05-07 19:45:34 -04:00
69251d7a0d docs(08): UI design contract 2026-05-07 17:50:19 -04:00
80bb9495fc docs(08): fix UI-SPEC checker failures — typography and spacing
- Collapse 6 font sizes to 4 (12, 14, 20, 24px): promote display name
  from text-lg (18px) to text-xl (20px); promote avatar initials and
  period chip text from text-[10px] to text-xs (12px)
- Remove non-standard "Additional fixed sizes" block; contract now
  declares exactly 4 canonical sizes
- Drop font-medium (500); breakdown subsection headers move to
  font-semibold (600) — two weights only: 400 + 600
- Replace py-1.5 (6px) with py-2 (8px) throughout breakdown rows;
  note D-16 override with rationale; clarify min-h-[44px] as WCAG
  floor only (not a spacing/padding value)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 17:48:10 -04:00
725fe731ca docs(08): UI design contract 2026-05-07 17:44:42 -04:00
f0d06ad06f docs(state): record phase 8 context session 2026-05-07 17:39:08 -04:00
1459b86a9d docs(08): capture phase context 2026-05-07 17:39:08 -04:00
a394517603 docs(phase-07.1): complete phase + evolve PROJECT.md after user approval 2026-05-07 17:26:04 -04:00
26fba8170a test(07.1): mark BUG-7.1-B resolved by 660d039 2026-05-07 17:21:44 -04:00
660d039b80 fix(07.1-02): allowlist UTC, Etc/UTC, GMT in IANA validator 2026-05-07 17:21:26 -04:00
f55b937af9 test(07.1): UAT results — 5 pass, 2 skipped, 2 bugs found (1 fixed) 2026-05-07 16:46:53 -04:00
d31fd48cad fix(07.1-02): use updatedAt camelCase in user timezone UPDATE 2026-05-07 16:45:52 -04:00
91ccf6560f docs(07.1): add phase verification report 2026-05-07 09:15:59 -04:00
15bb8dbd3a test(07.1): persist human verification items as UAT 2026-05-07 09:15:56 -04:00
e189c9a417 docs(07.1-05): complete codebase-wide tz adoption plan
- Mark all 51 manifest checklist entries as [x] complete (50 migrated +
  1 deferred-then-migrated via the audit's 50/51 path; 1 file rounds
  out the count via auvik-tab DEFER).
- Wait — accurate: 50 files migrated, 1 file deferred (auvik-tab),
  1 file deleted (.backup orphan). 80 of 81 audit leak callsites threaded
  with timeZone: tz; 1 deferred for layering boundary.
- Write 07.1-05-SUMMARY.md documenting the 8 task commits, per-pattern
  callsite counts, deferred rationale, and post-migration grep residue.
- Phase 7.1 SC#4 satisfied codebase-wide.
2026-05-07 08:47:12 -04:00
8f955a0ff9 feat(07.1-05): user-tz on shared client components
- DetailModal: thread tz through resolveLabel(...) module helper +
  default export's 3 inline date/time calls.
- IntegrationStatusTabs: thread tz through fmtDate helper +
  VeeamTab sub-component prop.
- SyncScheduler: thread tz into closure-scoped formatDate helper.
- audit-log-table, user-table, user-sessions, active-sessions: inline
  toLocale calls in component body.
- analysis-view: useUserTimezone in AnalysisView; thread tz into 4
  toLocaleString calls.
- resolution-trend, volume-trend (recharts): module-scope fmtDate(iso)
  → fmtDate(iso, tz); useUserTimezone in named export; thread tz into
  axis tickFormatter + tooltip labelFormatter.
- ticket-detail-modal: thread tz into formatDate arrow inside
  TicketDetailModal.
- TimelineView: useUserTimezone; thread tz into 4 toLocale*String calls
  (hour/day/month/event-time formatters).
- ScoreCard: useUserTimezone in AggregateScoreCard; thread tz into the
  date-range latest call.
- addigy-tab: useUserTimezone in AddigyTab; thread tz into 2 inline calls.
- activity-sparkline: module-scope fmtHour(iso) → fmtHour(iso, tz);
  useUserTimezone in ActivitySparkline; update 3 callsites in title/aria.
- compliance-detail-table: thread tz from ComplianceDetailTable into
  ContractCoverageModal sub-component (2 inline date calls).
- company-backup-detail: module-scope formatDate(d) → formatDate(d, tz);
  useUserTimezone in CompanyBackupDetail; update 3 callsites.

Migrates 31 of 81 audit leak callsites.
2026-05-07 08:43:27 -04:00
91b876310e feat(07.1-05): user-tz on dashboard, quotes, veeam-analysis
- dashboard/page.tsx: thread tz into PageHeader description's
  toLocaleDateString call.
- quotes/page.tsx: thread tz into formatDate arrow helper inside the
  default export.
- veeam-analysis/page.tsx: thread tz into the summary footer's
  generated-at toLocaleString call.

Migrates 3 of 81 audit leak callsites.
2026-05-07 08:36:09 -04:00
96edfb4444 feat(07.1-05): user-tz on analyzer pages
- itglue/applications, applications/[id], configurations,
  configurations/[id], sites/[companyId], queue, ticket/[ticketNumber],
  tickets, reports, reports/[id]: useUserTimezone() in default export;
  thread tz into every inline toLocale*String call.
- analyzer/tickets/page.tsx converts module-scope formatRelative(iso)
  helper to formatRelative(iso, tz); updates 1 callsite.

Migrates 16 of 81 audit leak callsites.
2026-05-07 08:34:58 -04:00
23b179f2a7 feat(07.1-05): user-tz on admin operational pages
- zabbix-wan, rmm-overshell, itglue-writes, ticket-digest,
  device-link-conflicts, workflow/history, workflow/pipelines/[id]:
  each gets useUserTimezone() at the component entry; threads tz into
  every inline toLocaleString call.

Migrates 11 of 81 audit leak callsites.
2026-05-07 08:31:22 -04:00
8c56cafe0b feat(07.1-05): user-tz on admin sync pages
- duo, sentinelone, datto-rmm, veeam, itglue, mimecast: each gets
  useUserTimezone() in default export and threads tz through
  fmtDate/sub-component props.
- duo (1 callsite, closure inline), sentinelone (1, module-scope helper),
  datto-rmm (1 helper + StatusTab/HistoryTab props), veeam (1 helper +
  5 sub-components), itglue (1 helper + StatusTab/HistoryTab props),
  mimecast (1 helper + 6 sub-components incl. 2 dialogs with inline
  toLocaleString calls).
- Module-scope fmtDate(d) signatures converted to fmtDate(d, tz).

Migrates 13 of 81 audit leak callsites.
2026-05-07 08:28:42 -04:00
a709144685 feat(07.1-05): user-tz on engagement overview + profile pages
- app/engagement/page.tsx: useUserTimezone in EngagementPage; thread tz
  into 7 toLocale* callsites (lines 844, 1012, 1108, 1227, 1255 — last
  two have 2 calls per line for date+time).
- app/engagement/profile/page.tsx: useUserTimezone in EngagementProfilePage;
  add tz prop to ActivityHeatmap; convert module-scope monthLabel(m) to
  monthLabel(m, tz); update 2 callsites of monthLabel.

Migrates 9 of 81 audit leak callsites.
2026-05-07 08:23:24 -04:00
b417988ee6 feat(07.1-05): user-tz on admin data-browser DataTable columns
- Add useUserTimezone() to 6 admin/data-browser pages
- Thread { timeZone: tz } into 8 DataTable column render() calls
- Delete orphaned app/admin/data-browser/time-entries/page.tsx.backup
  (per audit footnote — never imported, contained 1 leak at line 166)

Files: contracts, projects, tasks, ticket-notes, tickets, time-entries

Migrates 8 of 81 audit leak callsites.
2026-05-07 08:21:18 -04:00
82958c5ec6 docs(07.1-05): build leak migration manifest from audit
- 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)
2026-05-07 08:19:30 -04:00
36eba2e2af docs(07.1-03): complete user-tz server migration plan 2026-05-07 08:08:43 -04:00
04d036ab78 feat(07.1-03): user-tz day buckets on /api/dashboard/trends
- volumeRes / resolutionRes generate_series and join keys converted from
  CURRENT_DATE / *_date::date = days.d to user-tz two-step idiom.
- engineersRes WHERE filter te.entry_date::date = CURRENT_DATE migrated
  to user-tz on both sides.
- queueHeatmap (open-only counts) preserved unchanged — no day-boundary
  math; comment added explaining why.
- requireAuth() session destructured; tz passed as $1 to all three
  migrated queries.
2026-05-07 08:05:32 -04:00
dc0b06b9c7 feat(07.1-03): user-tz boundaries on /api/mobile/finance + engagement; auth-gate finance
- /api/mobile/finance: add requireAuth() (aligns with all other /api/mobile/*
  handlers) + getUserTimezone(); migrate paid_mtd / paid_ytd to user-tz
  DATE_TRUNC, six aging-bucket comparisons to user-tz CURRENT_DATE, and
  days_overdue arithmetic. Preserved unchanged: 12-month rolling
  monthlyRevenue (rolling — not a calendar boundary).
- /api/mobile/engagement/summary: destructure session, resolve tz; migrate
  rolling time_entries WHERE clause to user-tz on both sides of >=. Added
  TZ-02 carve-out comment above the snapshot queries documenting why
  engagement_snapshots remain UTC-bucketed (deferred per REQUIREMENTS.md).
- /api/mobile/engagement/trend: replace every bare CURRENT_DATE with
  (NOW() AT TIME ZONE 'UTC' AT TIME ZONE $1)::date; pass [tz] as params
  to postgresClient.query. Day buckets now align to user-tz days.
2026-05-07 08:04:47 -04:00
8a9887faa1 feat(07.1-03): user-tz day boundaries on /api/(mobile/)dashboard(/overview)
- Switch opened_today / resolved_today / yesterday / 7d-avg buckets from
  CURRENT_DATE to ((value AT TIME ZONE 'UTC') AT TIME ZONE $1)::date.
- Both routes destructure session from requireAuth() and resolve tz via
  getUserTimezone(); tz parameterized as $1 (no SQL interpolation).
- Preserved unchanged: due_date_time < NOW() (rolling SLA, tz-independent),
  the INTERVAL '24h/5min/1h' rolling-window queries (failed backups,
  stalled workflows, analyzer/RMM 1h fail counts, backup-success 24h).
- Added a code comment above the 24h failed-backups query explaining why
  it stays UTC-NOW relative.
2026-05-07 08:02:52 -04:00
ea5532c5c3 feat(07.1-03): add lib/services/user-timezone.ts helper
- getUserTimezone(session) returns validated IANA tz string with safe fallback
- DEFAULT_TIMEZONE_FALLBACK reads process.env.DEFAULT_TIMEZONE || 'UTC'
- Validates against Intl.supportedValuesOf('timeZone'); 64-char length cap
- Pure / synchronous / no DB / no @/lib/auth-utils import (avoids circular)
2026-05-07 08:01:23 -04:00
3f3142bbb7 docs(07.1-04): complete useUserTimezone hook + mobile migration plan
- Hook signature documented (useUserTimezone + formatInUserTimezone)
- Migrated callsite tables (before/after) for finance + tickets pages
- Audit results: 81 leak callsites across 39 files; Plan 05 dispatch = NEEDED
- All 3 task commits and acceptance criteria pass self-check
2026-05-07 07:58:42 -04:00
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