Commit graph

196 commits

Author SHA1 Message Date
7f4ffa0fb6 feat(09-06): add /admin/workflow/event-keys CRUD page and API routes
- GET/POST /api/admin/notify-event-keys: list ordered by sort_order/key, create with key regex validation (^[a-z][a-z0-9_]*$/i), 409 on conflict
- PUT/DELETE /api/admin/notify-event-keys/[key]: update via COALESCE, hard delete with 404 guard
- app/admin/workflow/event-keys/page.tsx: list with inline edit, Switch for is_active toggle, + New event key form, sonner toasts
- All routes gated by requireAdmin()
2026-05-10 07:41:27 -04:00
da13caf9cb feat(09-04): page shell, drawer wiring, skeleton helper, channels placeholder
- app/mobile/profile/page.tsx: server-component shell gated by requireAuth() + redirect('/auth/sign-in')
- ProfileSectionSkeleton.tsx: generic 3-row pulsing skeleton Card
- ProfileChannelsSectionPlaceholder.tsx: stub Channels card (Plan 05 swaps real component)
- MoreDrawer.tsx Account section: identity row wrapped in Link, new Profile & preferences row above Sign-out
2026-05-10 07:40:40 -04:00
47cab788fc feat(09-06): owner column + role-scoped reads on notification channels
- GET /api/notification-channels: requireAuth(), admin sees all rows with owner_email JOIN, non-admin sees global-only
- GET accepts ?owner=global|personal|all filter parameter
- POST /api/notification-channels: requireAdmin(); preserves all four channel_type values (teams/telegram/ntfy/webhook); adds owner_user_id column
- [id] routes: requireAuth() + per-row authorization (isAdmin || isOwner); global rows require admin
- Admin channels page: Owner badge (Global vs Personal: email), Show filter select, disclaimer text for personal channels
2026-05-10 07:39:53 -04:00
55a80a07ad feat(09-02): GET + PUT /api/me/notification-subscriptions (matrix endpoint)
- GET returns { eventKeys, channelTypes, matrix } where matrix defaults to
  true when no row exists (D-15 opt-out model)
- PUT UPSERTs single row via composite PK ON CONFLICT
- Validates: event_key (non-empty, <=128 chars), channel_type via
  isPersonalChannelType, enabled as typeof boolean
- Write target always session.user.id (T-09-02-01, T-09-02-03)
2026-05-10 07:30:51 -04:00
c35b968522 feat(09-02): personal channels service + /api/me/channels routes
- lib/services/personal-channels.ts: isValidTeamsWebhookUrl, isValidNtfyTopic,
  mintNtfyTopic, sendChannelTest, TEST_MESSAGE_BODY, isPersonalChannelType,
  PERSONAL_CHANNEL_TYPES
- GET /api/me/channels: returns user's personal channels (owner_user_id scoped)
- PUT /api/me/channels/[type]: WITH-CTE UPSERT + best-effort test send
- DELETE /api/me/channels/[type]: removes user's channel, 404 if missing
- POST /api/me/channels/[type]/test: re-sends test to existing channel
- SSRF mitigation via Teams URL hostname allowlist (T-09-02-06)
- Race window closed by partial unique index from Plan 01 (T-09-02-10)
2026-05-10 07:30:05 -04:00
dc5dc913bd feat(09-02): GET + PUT /api/me/theme
- ALLOWED_THEMES allowlist for light/dark/system
- GET returns { theme, source: 'user'|'default' }
- PUT validates against allowlist, writes session.user.id only
- Uses updated_at (snake_case) — matches migration 012 schema
- No userId from body (T-09-02-01 mitigation)
2026-05-10 07:28:03 -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
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
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
660d039b80 fix(07.1-02): allowlist UTC, Etc/UTC, GMT in IANA validator 2026-05-07 17:21:26 -04:00
d31fd48cad fix(07.1-02): use updatedAt camelCase in user timezone UPDATE 2026-05-07 16:45:52 -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
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
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
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
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
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
00d0102168 chore: merge executor worktree (07-01) 2026-05-03 22:45:36 -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
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
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
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
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
5d87455e49 feat(04-02): rewrite mobile tickets list page with URL-synced filters, priority stripes, and IntersectionObserver infinite scroll
Cherry-picked from 04-02 worktree (b27db7d, 4ba89a0). The full worktree merge
included unrelated regressions of phase 2/3 work — only the intended changes
(app/mobile/tickets/page.tsx + SUMMARY.md) were kept.

- Suspense wrapper for useSearchParams (Next.js 16 requirement)
- URL-synced filter state via router.replace
- TicketFilterStrip wired with controlled props
- Priority-stripe rows (border-l-4 with red/orange/amber/slate)
- IntersectionObserver infinite scroll + Load more fallback
- toast.error() in load catch blocks per D-21
- Empty states per D-20
2026-05-03 18:12:00 -04:00
422ea2bd0e chore: merge executor worktree (04-03) 2026-05-03 18:04:35 -04:00
9658640c04 fix(04-01): restore phase 2/3 work lost by worktree soft-reset
The soft reset to 77073ba inadvertently staged deletions of all phase 2
and 3 artifacts. This commit restores them from their source commits so
subsequent task commits build on the complete prior-phase foundation:
- components/mobile/{BottomNav,HeaderBar,KpiCardMobile,MoreDrawer,NeedsAttentionStrip,WorkerStatusRow}
- app/mobile/layout.tsx, dashboard/page.tsx, analyzer/page.tsx
- app/api/mobile/dashboard/route.ts
- All .planning/** files from phases 01-04
- CLAUDE.md, app/layout.tsx, app/styles/brand.css, public/manifest.json
2026-05-03 18:01:14 -04:00
6268d1fe37 feat(04-01): rewrite /api/mobile/tickets with cursor pagination and exported interfaces
- Replace page/offset pagination with opaque base64 cursor (last_activity_date, id)
- Export MobileTicket and MobileTicketListResponse interfaces for Plan 02 import
- Add requireAuth() gate (T-04-03: legacy route lacked auth)
- Server-side limit cap at 25 rows (D-11, T-04-04)
- Default status filter [1,8,7] when no status param supplied (matches legacy t.status != 5)
- Preserve getMobileCompanyFilter() helper verbatim
- Support status/priority arrays, queue, mine, and search filters
- Cursor seek predicate: (last_activity_date, id) < (cursor) for stable keyset order
2026-05-03 17:59:54 -04:00
b1ff8660d4 feat(04-03): reskin /mobile/tickets/[id] in-page header per D-18
- Add ExternalLink to lucide-react imports
- Replace legacy 'Back' button with three-slot header bar:
  back chevron + 'Tickets' label (router.back()), breadcrumb
  'Tickets / #{ticket_number}', ExternalLink icon to /analyzer/ticket/{id}
- rel='noopener noreferrer' + target='_blank' on external link (T-04-11)
- Detail body (badges, title, stats grid, description, timeline) unchanged per D-19
2026-05-03 17:58:51 -04:00
52562503c0 feat(03-02): replace mobile dashboard page with 3-section layout
- Drop legacy recharts/chart sections, priority breakdown, SLA bars, queue list, recent activity
- Add 2x2 KPI grid (DASH-01), Needs Attention horizontal strip (DASH-02), Worker/backup status row (DASH-03)
- Wire KpiCardMobile, NeedsAttentionStrip, WorkerStatusRow against /api/mobile/dashboard
- Inline error state with Retry button, spinner while loading, refresh button in H1 row
- Zero recharts imports (DASH-04); 118 lines
2026-05-03 16:53:35 -04:00
24e20c7ae7 feat(03-01): rewrite /api/mobile/dashboard to return kpis/needsAttention/workers shape
- Exports MobileDashboardResponse, KpiResponse, AttentionResponse, WorkerResponse interfaces
- Single Promise.all with 6 parameterless queries (KPI, failed backups, stalled workflows, analyzer, RMM, backup success)
- Ticket KPIs exclude out-of-scope companies via company_scope filter
- SLA breaches tone='attention' when value > 0
- Worker status rules: down if fail_1h>0 and in_flight=0, warn if fail_1h>0, otherwise ok
- Backup status: ok >= 95%, warn >= 80%, down otherwise
2026-05-03 16:49:49 -04:00
2af73955ae feat(02-02): delete app/mobile/nav/page.tsx (DRAWER-06)
- Standalone nav page replaced by MoreDrawer Sheet component
- Visiting /mobile/nav now returns Next.js 404 per DRAWER-06 spec
- No redirect needed — URL was never bookmarked-worthy
2026-05-03 16:09:51 -04:00
7a095fbe53 feat(02-02): rewrite mobile layout to wire HeaderBar + BottomNav + MoreDrawer
- Replace legacy 3-tab layout with new shell components from Plan 01
- Single useState(drawerOpen) shared between avatar and More button triggers
- SHELL-05: main content padded pb-[calc(theme(spacing.16)+env(safe-area-inset-bottom))]
- max-w-lg mx-auto container, no legacy /mobile/nav reference
2026-05-03 16:09:35 -04:00
bb5b62af86 feat(02-01): add /mobile/analyzer placeholder so Analyzer tab does not 404
- Minimal server component with coming soon card
- No data fetching — Phase 6 (ANL-01..06) owns the real implementation
- Sparkles icon consistent with BottomNav tab icon
- Resolves NAV-02: Analyzer tab in bottom nav has a valid route
2026-05-03 16:08:00 -04:00
dff0264115 feat(01-02): add pt-safe and pb-safe @utility blocks for safe-area insets
- Append @utility pt-safe { padding-top: env(safe-area-inset-top); }
- Append @utility pb-safe { padding-bottom: env(safe-area-inset-bottom); }
- Co-located with existing brand utilities; brand.css already imported by globals.css
- Closes PWA-04 (REQUIREMENTS.md) and ROADMAP Phase 1 SC #3
- Phase 2 sticky header → pt-safe; fixed bottom nav → pb-safe
2026-05-03 13:56:21 -04:00
d196d22040 feat(01-01): add manifest reference and viewport export to root layout
- Add metadata.manifest = "/manifest.json" so Next.js emits
  <link rel="manifest"> automatically (PWA-02)
- Add separate viewport export (Next 16 API) with viewportFit: "cover"
  so Phase 2 can paint behind the home indicator (PWA-03)
- Include width: "device-width", initialScale: 1, and themeColor
  light/dark pair for clean mobile rendering
- RootLayout body unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:38:44 -04:00
db375fb0e6 feat(admin): client scope — filter analytics to recurring-revenue companies
Adds company-level opt-out scoping so white-label / subcontract clients
(TTG, LEC, PER, VCF, Trivium Packaging, TNT Pizza, etc.) can be excluded
from Wulf's own dashboard KPIs and ticket analytics without affecting
per-company drill-down views.

- migration 082: company_scope table (opt-out; absent row = in scope)
- GET/PATCH /api/admin/company-scope[/companyId] — list + upsert
- /admin/client-scope — searchable company list with Switch per row,
  type filter, and in/out scope filter; excluded rows are dimmed
- dashboard overview KPIs now exclude out-of-scope company tickets
- analyzer /tickets query excludes out-of-scope when no specific
  client is selected (explicit per-company selection still works)
- "Client Scope" tile added to admin Tools & Data section

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:40:47 -04:00
370125d249 feat(admin): group integration toggles by category
Bucket the integration list under category headers (PSA, RMM,
Documentation, Security, …) following a canonical order, with
unrecognized categories appended after. Rows within a bucket sort
alphabetically by name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:03:54 -04:00
344276751e refactor(nav): adopt PageHeader on /configuration-items
The page was rendering its own sticky <header> on top of the global
AppNavigation, which obscured the top nav and duplicated the
ThemeToggle and a redundant "Back to Dashboard" link.

Replace with PageHeader (title, description, breadcrumbs, actions slot
holding Refresh / Bulk-admin / Export) and switch the main container to
the conventional px-6 py-6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:03:49 -04:00