Commit graph

334 commits

Author SHA1 Message Date
4fc4a3d3b9 feat(09-01): add theme column to user table + Better Auth additionalField
- migrations/084_add_user_theme.sql: ALTER TABLE user ADD COLUMN theme TEXT NOT NULL DEFAULT 'system'
- Defensive backfill UPDATE for in-flight NULL rows on managed Postgres
- COMMENT ON COLUMN documents allowed values (light|dark|system)
- lib/auth.ts: adds theme additionalField with defaultValue 'system' after timezone
- session.user.theme now exposed via Better Auth same as session.user.timezone
2026-05-10 07:22:26 -04:00
e5031e5613 docs(09): create phase plan 2026-05-09 22:54:02 -04:00
480c115296 docs(09): UI design contract 2026-05-09 22:09:45 -04:00
31ff615752 docs(09): fix two blocking UI-SPEC issues found by checker
- Typography: remove text-base (16px) reference for Card titles; map
  them to the declared 20px Heading role (text-xl) to keep scale at
  exactly 4 sizes. Add explicit note that section header labels use
  the 12px Label role, not a fifth size.
- Copywriting: add explicit "Save custom topic" row to Channels table
  for the ntfy advanced-disclosure save button; also propagate the
  label into the layout block and the save-model table so executor
  has no ambiguity.
- Visuals (optional flag): declare page focal point — H1 is entry
  anchor, Channels Card is conversion-weight section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:08:01 -04:00
d6a9eaca8f docs(09): UI design contract 2026-05-09 22:03:29 -04:00
d6a6c7139b docs(09): formalize phase 9 requirements after discuss-phase 2026-05-09 16:54:23 -04:00
039033ddff docs(state): record phase 9 context session 2026-05-09 15:06:44 -04:00
e383e986d0 docs(09): capture phase context 2026-05-09 15:06:40 -04:00
84352b3d61 docs(phase-08): evolve PROJECT.md after phase completion 2026-05-08 09:58:54 -04:00
a5e43fddbf docs(phase-08): complete phase execution 2026-05-08 09:58:32 -04:00
25f03cd9d1 test(08): persist human verification items as UAT 2026-05-08 09:47:25 -04:00
7fcb156cfc docs(08-02): SUMMARY — mobile engagement profile page complete (SC#2 partial) 2026-05-07 23:06:44 -04:00
435051ddc8 fix(08-02): retry scroll restoration across frames until layout finalizes
The window scrolls (not <main>) on this layout, and the document content
height isn't fully laid out by the first rAF after rows render — so
window.scrollTo gets clamped to maxScroll, leaving the user near top.
Retry up to 30 frames (~500ms) until the actual scroll position matches
the target within 4px.
2026-05-07 23:01:22 -04:00
8834db981d fix(08-02): scroll restoration — save/restore both window and <main> scrollTop, defer to rAF after rows render 2026-05-07 22:23:48 -04:00
6bdc937861 fix(08-02): preserve engagement list scroll across profile navigation (D-04)
The mobile shell's <main> is overflow-y-auto, so Next.js's built-in
scrollRestoration (window-only) doesn't restore the list's inner scroll
when returning from /mobile/engagement/[userId]. Persist the scroll
position to sessionStorage on scroll (rAF-throttled) and restore once
after the first users page loads.

Restoration is gated to the first load only, so changing period/sort
doesn't yank the viewport — and uses a single sessionStorage key, so
returning to the list later still lands where you were.
2026-05-07 22:10:43 -04:00
81079ad89f fix(08-02): coerce Postgres numeric hours_worked to Number before toFixed
Postgres returns NUMERIC columns as strings via pg, so calling
.toFixed(1) on time_entries.hours_worked from /api/engagement/user/[userId]
threw at runtime. The period-level hours in the same response are already
parseFloat'd; the recentEntries and matchedEntries arrays pass rows through
verbatim, so wrap with Number() at render.
2026-05-07 22:02:52 -04:00
b3ad92cd86 docs: plant seed — Wulf Standards Alignment Engine 2026-05-07 21:10:24 -04:00
0be0c1f7f8 feat(08-02): activity breakdown + recent entries + meetings + page wiring (Task 2)
- New EngagementProfileBreakdown: Time/Communication/Meetings subsections, after-hours
  and Zoom conditional rows, py-2 per UI-SPEC override
- New EngagementRecentEntries: collapsible list up to 10, Billable badge, Set<string>
  expand state, empty-state copy
- New EngagementRecentMeetings: collapsible list up to 10, matched entries + attendees
  in expanded view, (no subject) fallback, Set<string> expand state
- Page updated: 3 new component imports + breakdown/entries/meetings mounted in order
- No dangerouslySetInnerHTML; D-01/D-22 guard rails untouched
2026-05-07 20:50:56 -04:00
df78ab8fa5 feat(08-02): identity header + 2x2 metric grid + page wiring (Task 1b)
- New EngagementProfileHeader: avatar (photo/initials fallback), name, jobTitle,
  department, mailto link, last-active relative/absolute label
- New EngagementProfileMetricGrid: 2x2 grid of Hours/Billable/Days/Meetings cards
- Page updated: imports Header+MetricGrid, placeholder div removed, real components mounted
- D-01/D-22 guard rails: EngagementUserRow.tsx and data endpoint untouched
2026-05-07 20:48:24 -04:00
3247c92486 feat(08-02): page shell + skeleton + period/fetch wiring (Task 1a)
- New app/mobile/engagement/[userId]/page.tsx with fetch + error states + retryNonce
- New EngagementProfileSkeleton with header/metric/breakdown/list skeletons
- 404 renders 'User not found' + back link; 500 renders sonner toast + Retry
- D-04 comment: relies on App Router default scrollRestoration
- D-01/D-22 guard rails: EngagementUserRow.tsx and data endpoint untouched
2026-05-07 20:46:28 -04:00
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