Files
breakpilot-core/pitch-deck/lib/hooks/useAuditTracker.ts
Sharang Parnerkar 645973141c
All checks were successful
CI / test-go-consent (push) Successful in 27s
CI / test-python-voice (push) Successful in 25s
CI / test-bqas (push) Successful in 27s
CI / Deploy (push) Successful in 6s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
feat(pitch-deck): passwordless investor auth, audit logs, snapshots & PWA (#2)
Adds investor-facing access controls, persistence, and PWA support to the pitch deck:

- Passwordless magic-link auth (jose JWT + nodemailer SMTP)
- Per-investor audit logging (logins, slide views, assumption changes, chat)
- Financial model snapshot persistence (auto-save/restore per investor)
- PWA support (manifest, service worker, offline caching, branded icons)
- Safeguards: email watermark overlay, security headers, content protection,
  rate limiting, IP/new-IP detection, single active session per investor
- Admin API: invite, list investors, revoke, query audit logs
- pitch-deck service added to docker-compose.coolify.yml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:48:38 +00:00

74 lines
2.2 KiB
TypeScript

'use client'
import { useEffect, useRef, useCallback } from 'react'
interface AuditTrackerOptions {
investorId: string | null
currentSlide: string
enabled: boolean
}
export function useAuditTracker({ investorId, currentSlide, enabled }: AuditTrackerOptions) {
const lastSlide = useRef<string>('')
const slideTimestamps = useRef<Map<string, number>>(new Map())
const pendingEvents = useRef<Array<{ action: string; details: Record<string, unknown>; slide_id?: string }>>([])
const flushTimer = useRef<NodeJS.Timeout | null>(null)
const flush = useCallback(async () => {
if (pendingEvents.current.length === 0) return
const events = [...pendingEvents.current]
pendingEvents.current = []
// Send events one at a time (they're debounced so there shouldn't be many)
for (const event of events) {
try {
await fetch('/api/audit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
})
} catch {
// Silently fail - audit should not block UX
}
}
}, [])
const track = useCallback((action: string, details: Record<string, unknown> = {}, slideId?: string) => {
if (!enabled || !investorId) return
pendingEvents.current.push({ action, details, slide_id: slideId })
// Debounce flush by 500ms
if (flushTimer.current) clearTimeout(flushTimer.current)
flushTimer.current = setTimeout(flush, 500)
}, [enabled, investorId, flush])
// Track slide views
useEffect(() => {
if (!enabled || !investorId || !currentSlide) return
if (currentSlide === lastSlide.current) return
const now = Date.now()
const prevTimestamp = slideTimestamps.current.get(lastSlide.current)
const dwellTime = prevTimestamp ? now - prevTimestamp : 0
lastSlide.current = currentSlide
slideTimestamps.current.set(currentSlide, now)
track('slide_viewed', {
slide_id: currentSlide,
previous_dwell_ms: dwellTime,
}, currentSlide)
}, [currentSlide, enabled, investorId, track])
// Flush on unmount
useEffect(() => {
return () => {
if (flushTimer.current) clearTimeout(flushTimer.current)
flush()
}
}, [flush])
return { track }
}