import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Fragment, useState } from 'react'; import { getScan, getScanDiff, listScans, triggerScan } from '../api/scanner'; import { getSiteConfig, updateSiteConfig } from '../api/sites'; import { trackFeatureUsage } from '../services/analytics'; import type { CookieDiffItem, ScanDiff, ScanJob, ScanJobDetail, ScanResult, SiteConfig } from '../types/api'; import { Alert } from './ui/alert'; import { Badge } from './ui/badge'; import { Button } from './ui/button'; import { Card } from './ui/card'; import { LoadingState } from './ui/loading-state'; import { Select } from './ui/select'; interface Props { siteId: string; } const SCHEDULE_OPTIONS: { value: string; label: string; cron: string | null }[] = [ { value: 'disabled', label: 'Disabled', cron: null }, { value: 'daily', label: 'Daily', cron: '0 3 * * *' }, { value: 'weekly', label: 'Weekly', cron: '0 3 * * 0' }, { value: 'fortnightly', label: 'Fortnightly', cron: '0 3 1,15 * *' }, { value: 'monthly', label: 'Monthly', cron: '0 3 1 * *' }, ]; function cronToScheduleValue(cron: string | null | undefined): string { if (!cron) return 'disabled'; const match = SCHEDULE_OPTIONS.find((o) => o.cron === cron); return match?.value ?? 'custom'; } function statusVariant(status: string): 'warning' | 'info' | 'success' | 'error' | 'neutral' { const map: Record = { pending: 'warning', running: 'info', completed: 'success', failed: 'error', }; return map[status] ?? 'neutral'; } function diffVariant(status: string): 'success' | 'error' | 'warning' | 'neutral' { const map: Record = { new: 'success', removed: 'error', changed: 'warning', }; return map[status] ?? 'neutral'; } function DiffSection({ title, items }: { title: string; items: CookieDiffItem[] }) { if (items.length === 0) return null; return (

{title} ({items.length})

{items.map((item, idx) => ( ))}
Name Domain Type Status Details
{item.name} {item.domain} {item.storage_type} {item.diff_status} {item.details ?? '—'}
); } function ScanDiffView({ scanId }: { scanId: string }) { const { data: diff, isLoading } = useQuery({ queryKey: ['scans', scanId, 'diff'], queryFn: () => getScanDiff(scanId), }); if (isLoading) return ; if (!diff) return null; const hasChanges = diff.total_new + diff.total_removed + diff.total_changed > 0; return (

Scan Diff {diff.previous_scan_id ? '' : ' (first scan — no comparison available)'}

{hasChanges ? ( <> ) : (

No changes detected.

)}
); } function InitiatorChain({ chain }: { chain: string[] }) { if (chain.length === 0) return ; return (
{chain.map((url, idx) => { // Show just the pathname for brevity let label: string; try { const parsed = new URL(url); label = parsed.pathname.length > 40 ? '…' + parsed.pathname.slice(-38) : parsed.pathname; } catch { label = url.length > 40 ? '…' + url.slice(-38) : url; } return ( {idx > 0 && } {label} ); })}
); } function ScanResultsView({ scanId }: { scanId: string }) { const { data: detail, isLoading } = useQuery({ queryKey: ['scans', scanId, 'detail'], queryFn: () => getScan(scanId), }); if (isLoading) return ; if (!detail || detail.results.length === 0) { return

No results recorded.

; } // Only show results that have an initiator chain const withChain = detail.results.filter( (r: ScanResult) => r.initiator_chain && r.initiator_chain.length > 1, ); if (withChain.length === 0) { return

No initiator chains detected in this scan.

; } return (

Initiator Chains ({withChain.length} cookies)

{withChain.map((r: ScanResult) => ( ))}
Cookie Domain Chain
{r.cookie_name} {r.cookie_domain}
); } export default function SiteScannerTab({ siteId }: Props) { const queryClient = useQueryClient(); const [expandedScanId, setExpandedScanId] = useState(null); const { data: config } = useQuery({ queryKey: ['sites', siteId, 'config'], queryFn: () => getSiteConfig(siteId), }); const currentCron = config?.scan_schedule_cron ?? null; const savedValue = cronToScheduleValue(currentCron); const [selectedSchedule, setSelectedSchedule] = useState(null); const [customCron, setCustomCron] = useState(''); // Use local selection if the user has interacted, otherwise fall // back to what's saved on the server. const activeValue = selectedSchedule ?? savedValue; const showCustomInput = activeValue === 'custom'; const scheduleMutation = useMutation({ mutationFn: (cron: string | null) => updateSiteConfig(siteId, { scan_schedule_cron: cron } as Partial), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sites', siteId, 'config'] }); trackFeatureUsage('scan', 'schedule_change', { site_id: siteId }); setSelectedSchedule(null); // reset to server state }, }); const handleScheduleChange = (value: string) => { setSelectedSchedule(value); if (value === 'custom') { setCustomCron(currentCron ?? ''); return; } const option = SCHEDULE_OPTIONS.find((o) => o.value === value); scheduleMutation.mutate(option?.cron ?? null); }; const handleCustomSave = () => { const trimmed = customCron.trim(); scheduleMutation.mutate(trimmed || null); }; const { data: scans, isLoading } = useQuery({ queryKey: ['scans', siteId], queryFn: () => listScans(siteId), }); const triggerMutation = useMutation({ mutationFn: () => triggerScan(siteId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['scans', siteId] }); trackFeatureUsage('scan', 'trigger', { site_id: siteId }); }, }); if (isLoading) { return ; } return (
{/* Scan schedule */}

Scan Schedule

Scheduled scans run automatically and re-discover cookies so your inventory stays current. Select a preset or enter a custom cron expression.

{showCustomInput && ( <> setCustomCron(e.target.value)} /> Need help? Use crontab.guru → )} {scheduleMutation.isPending && ( Saving… )}
{currentCron && (

Current schedule: {currentCron}

)}
{/* Header with trigger button */}

Cookie Scans

{triggerMutation.isError && ( Failed to trigger scan. A scan may already be in progress. )} {/* Scan history */} {!scans || scans.length === 0 ? (
No scans yet. Trigger a scan to discover cookies on your site.
) : (
{scans.map((scan) => ( {expandedScanId === scan.id && ( )} ))}
Status Trigger Pages Cookies Found Started Completed Actions
{scan.status} {scan.trigger} {scan.pages_scanned}{scan.pages_total ? ` / ${scan.pages_total}` : ''} {scan.cookies_found} {scan.started_at ? new Date(scan.started_at).toLocaleString() : '—'} {scan.completed_at ? new Date(scan.completed_at).toLocaleString() : '—'} {scan.status === 'completed' && ( )} {scan.status === 'failed' && scan.error_message && ( Error )}
)}
); }