Files
breakpilot-core/pitch-deck/lib/hooks/useAuditTracker.ts
Sharang Parnerkar f565dfdb15
Some checks failed
CI / go-lint (pull_request) Failing after 17s
CI / python-lint (pull_request) Failing after 12s
CI / nodejs-lint (pull_request) Failing after 7s
CI / test-go-consent (pull_request) Failing after 11s
CI / test-python-voice (pull_request) Failing after 11s
CI / test-bqas (pull_request) Failing after 11s
CI / Deploy (pull_request) Has been skipped
feat(pitch-deck): add passwordless investor auth, audit logs, snapshots & PWA
Implement a complete investor access system for the pitch deck:

- Passwordless magic link auth (jose JWT + nodemailer SMTP)
- Per-investor audit logging (slide views, assumption changes, chat)
- Financial model snapshot persistence (auto-save/restore per investor)
- PWA support (manifest, service worker, offline caching, icons)
- Security safeguards (watermark overlay, rate limiting, anti-scraping
  headers, content protection, single-session enforcement)
- Admin API for invite/revoke/audit-log management
- Integrated into docker-compose.coolify.yml for production deployment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:37:50 +02: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 }
}