'use client' import { useEffect, useState, useRef } from 'react' import { useParams, useRouter, useSearchParams } from 'next/navigation' import { updateSite, resetSiteData, type Site, type GeoDataLevel } from '@/lib/api/sites' import { createGoal, updateGoal, deleteGoal, type Goal } from '@/lib/api/goals' import { createReportSchedule, updateReportSchedule, deleteReportSchedule, testReportSchedule, listAlertSchedules, type ReportSchedule, type CreateReportScheduleRequest, type EmailConfig, type WebhookConfig } from '@/lib/api/report-schedules' import { botFilterSessions, botUnfilterSessions } from '@/lib/api/bot-filter' import { getGSCAuthURL, disconnectGSC } from '@/lib/api/gsc' import { getBunnyPullZones, connectBunny, disconnectBunny } from '@/lib/api/bunny' import type { BunnyPullZone } from '@/lib/api/bunny' import { toast, getDateRange } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@ciphera-net/ui' import { formatDateTime } from '@/lib/utils/formatDate' import { SettingsFormSkeleton, GoalsListSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons' import VerificationModal from '@/components/sites/VerificationModal' import DeleteSiteModal from '@/components/sites/DeleteSiteModal' import ScriptSetupBlock from '@/components/sites/ScriptSetupBlock' import { PasswordInput } from '@ciphera-net/ui' import { Select, Modal, Button } from '@ciphera-net/ui' import { APP_URL } from '@/lib/api/client' import { generatePrivacySnippet } from '@/lib/utils/privacySnippet' import { useUnsavedChanges } from '@/lib/hooks/useUnsavedChanges' import { useSite, useGoals, useReportSchedules, useAlertSchedules, useSubscription, useGSCStatus, useBunnyStatus, useSessions, useBotFilterStats, usePageSpeedConfig } from '@/lib/swr/dashboard' import { updatePageSpeedConfig } from '@/lib/api/pagespeed' import { getRetentionOptionsForPlan, formatRetentionMonths } from '@/lib/plans' import { motion, AnimatePresence } from 'framer-motion' import { useAuth } from '@/lib/auth/context' import { SettingsIcon, GlobeIcon, CheckIcon, AlertTriangleIcon, ZapIcon, } from '@ciphera-net/ui' import { PaperPlaneTilt, Envelope, WebhooksLogo, SpinnerGap, Trash, PencilSimple, Play, Plugs, ShieldCheck, Bug, BellSimple } from '@phosphor-icons/react' import { SiDiscord } from '@icons-pack/react-simple-icons' function SlackIcon({ size = 16 }: { size?: number }) { return ( ) } const CHANNEL_ICONS: Record = { email: , slack: , discord: , webhook: , } const CHANNEL_ICONS_LG: Record = { email: , slack: , discord: , webhook: , } const TIMEZONES = [ 'UTC', 'America/New_York', 'America/Los_Angeles', 'America/Chicago', 'America/Toronto', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Amsterdam', 'Asia/Tokyo', 'Asia/Singapore', 'Asia/Dubai', 'Australia/Sydney', 'Pacific/Auckland', ] export default function SiteSettingsPage() { const { user } = useAuth() const canEdit = user?.role === 'owner' || user?.role === 'admin' const params = useParams() const router = useRouter() const siteId = params.id as string const { data: site, isLoading: siteLoading, mutate: mutateSite } = useSite(siteId) const [saving, setSaving] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) const [activeTab, setActiveTab] = useState<'general' | 'visibility' | 'data' | 'bot' | 'goals' | 'notifications' | 'integrations'>('general') const searchParams = useSearchParams() const [formData, setFormData] = useState({ name: '', timezone: 'UTC', is_public: false, password: '', excluded_paths: '', // Data collection settings collect_page_paths: true, collect_referrers: true, collect_device_info: true, collect_geo_data: 'full' as GeoDataLevel, collect_screen_resolution: true, // Bot and noise filtering filter_bots: true, // Hide unknown locations hide_unknown_locations: false, // Data retention (6 = free-tier max; safe default) data_retention_months: 6 }) const { data: subscription, error: subscriptionError, mutate: mutateSubscription } = useSubscription() const [linkCopied, setLinkCopied] = useState(false) const [snippetCopied, setSnippetCopied] = useState(false) const [showVerificationModal, setShowVerificationModal] = useState(false) const [isPasswordEnabled, setIsPasswordEnabled] = useState(false) const { data: goals = [], isLoading: goalsLoading, mutate: mutateGoals } = useGoals(siteId) const [goalModalOpen, setGoalModalOpen] = useState(false) const [editingGoal, setEditingGoal] = useState(null) const [goalForm, setGoalForm] = useState({ name: '', event_name: '' }) const [goalSaving, setGoalSaving] = useState(false) const initialFormRef = useRef('') // Report schedules const { data: reportSchedules = [], isLoading: reportLoading, mutate: mutateReportSchedules } = useReportSchedules(siteId) // Alert schedules (uptime alerts) const { data: alertSchedules = [], isLoading: alertLoading, mutate: mutateAlertSchedules } = useAlertSchedules(siteId) const { data: gscStatus, mutate: mutateGSCStatus } = useGSCStatus(siteId) const [gscConnecting, setGscConnecting] = useState(false) const [gscDisconnecting, setGscDisconnecting] = useState(false) const { data: bunnyStatus, mutate: mutateBunnyStatus } = useBunnyStatus(siteId) const { data: psiConfig, mutate: mutatePSIConfig } = usePageSpeedConfig(siteId) const [bunnyApiKey, setBunnyApiKey] = useState('') const [bunnyPullZones, setBunnyPullZones] = useState([]) const [bunnySelectedZone, setBunnySelectedZone] = useState(null) const [bunnyLoadingZones, setBunnyLoadingZones] = useState(false) const [bunnyConnecting, setBunnyConnecting] = useState(false) const [bunnyDisconnecting, setBunnyDisconnecting] = useState(false) const [reportModalOpen, setReportModalOpen] = useState(false) const [editingSchedule, setEditingSchedule] = useState(null) const [reportSaving, setReportSaving] = useState(false) const [reportTesting, setReportTesting] = useState(null) const [reportForm, setReportForm] = useState({ channel: 'email' as string, recipients: '', webhookUrl: '', frequency: 'weekly' as string, reportType: 'summary' as string, timezone: '', sendHour: 9, sendDay: 1, }) // Alert channel state const [alertModalOpen, setAlertModalOpen] = useState(false) const [editingAlert, setEditingAlert] = useState(null) const [alertSaving, setAlertSaving] = useState(false) const [alertTesting, setAlertTesting] = useState(null) const [alertForm, setAlertForm] = useState({ channel: 'email' as string, recipients: '', webhookUrl: '', }) // Bot & Spam tab state const [botDateRange, setBotDateRange] = useState(() => getDateRange(7)) const [suspiciousOnly, setSuspiciousOnly] = useState(true) const [selectedSessions, setSelectedSessions] = useState>(new Set()) const [botView, setBotView] = useState<'review' | 'blocked'>('review') const { data: sessions, mutate: mutateSessions } = useSessions(siteId, botDateRange.start, botDateRange.end, botView === 'review' ? suspiciousOnly : false) const { data: botStats, mutate: mutateBotStats } = useBotFilterStats(siteId) const handleBotFilter = async (sessionIds: string[]) => { try { await botFilterSessions(siteId, sessionIds) toast.success(`${sessionIds.length} session(s) flagged as bot`) setSelectedSessions(new Set()) mutateSessions() mutateBotStats() } catch { toast.error('Failed to flag sessions') } } const handleBotUnfilter = async (sessionIds: string[]) => { try { await botUnfilterSessions(siteId, sessionIds) toast.success(`${sessionIds.length} session(s) unblocked`) setSelectedSessions(new Set()) mutateSessions() mutateBotStats() } catch { toast.error('Failed to unblock sessions') } } useEffect(() => { if (!site) return setFormData({ name: site.name, timezone: site.timezone || 'UTC', is_public: site.is_public || false, password: '', excluded_paths: (site.excluded_paths || []).join('\n'), collect_page_paths: site.collect_page_paths ?? true, collect_referrers: site.collect_referrers ?? true, collect_device_info: site.collect_device_info ?? true, collect_geo_data: site.collect_geo_data || 'full', collect_screen_resolution: site.collect_screen_resolution ?? true, filter_bots: site.filter_bots ?? true, hide_unknown_locations: site.hide_unknown_locations ?? false, data_retention_months: site.data_retention_months ?? 6 }) initialFormRef.current = JSON.stringify({ name: site.name, timezone: site.timezone || 'UTC', is_public: site.is_public || false, excluded_paths: (site.excluded_paths || []).join('\n'), collect_page_paths: site.collect_page_paths ?? true, collect_referrers: site.collect_referrers ?? true, collect_device_info: site.collect_device_info ?? true, collect_geo_data: site.collect_geo_data || 'full', collect_screen_resolution: site.collect_screen_resolution ?? true, filter_bots: site.filter_bots ?? true, hide_unknown_locations: site.hide_unknown_locations ?? false, data_retention_months: site.data_retention_months ?? 6 }) setIsPasswordEnabled(!!site.has_password) }, [site]) // * Snap data_retention_months to nearest valid option when subscription loads useEffect(() => { if (!subscription) return const opts = getRetentionOptionsForPlan(subscription.plan_id) const values = opts.map(o => o.value) const maxVal = Math.max(...values) setFormData(prev => { if (values.includes(prev.data_retention_months)) return prev const bestFit = values.filter(v => v <= prev.data_retention_months).pop() ?? maxVal return { ...prev, data_retention_months: Math.min(bestFit, maxVal) } }) }, [subscription]) const resetReportForm = () => { setReportForm({ channel: 'email', recipients: '', webhookUrl: '', frequency: 'weekly', reportType: 'summary', timezone: site?.timezone || '', sendHour: 9, sendDay: 1, }) } const openEditSchedule = (schedule: ReportSchedule) => { setEditingSchedule(schedule) const isEmail = schedule.channel === 'email' setReportForm({ channel: schedule.channel, recipients: isEmail ? (schedule.channel_config as EmailConfig).recipients.join(', ') : '', webhookUrl: !isEmail ? (schedule.channel_config as WebhookConfig).url : '', frequency: schedule.frequency, reportType: schedule.report_type, timezone: schedule.timezone || site?.timezone || '', sendHour: schedule.send_hour ?? 9, sendDay: schedule.send_day ?? (schedule.frequency === 'monthly' ? 1 : 0), }) setReportModalOpen(true) } const handleReportSubmit = async (e: React.FormEvent) => { e.preventDefault() let channelConfig: EmailConfig | WebhookConfig if (reportForm.channel === 'email') { const recipients = reportForm.recipients.split(',').map(r => r.trim()).filter(r => r.length > 0) if (recipients.length === 0) { toast.error('At least one recipient email is required') return } channelConfig = { recipients } } else { if (!reportForm.webhookUrl.trim()) { toast.error('Webhook URL is required') return } channelConfig = { url: reportForm.webhookUrl.trim() } } const payload: CreateReportScheduleRequest = { channel: reportForm.channel, channel_config: channelConfig, frequency: reportForm.frequency, timezone: reportForm.timezone || undefined, report_type: reportForm.reportType, send_hour: reportForm.sendHour, ...(reportForm.frequency !== 'daily' ? { send_day: reportForm.sendDay } : {}), } setReportSaving(true) try { if (editingSchedule) { await updateReportSchedule(siteId, editingSchedule.id, payload) toast.success('Report schedule updated') } else { await createReportSchedule(siteId, payload) toast.success('Report schedule created') } setReportModalOpen(false) mutateReportSchedules() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to save report schedule') } finally { setReportSaving(false) } } const handleReportDelete = async (schedule: ReportSchedule) => { if (!confirm('Delete this report schedule?')) return try { await deleteReportSchedule(siteId, schedule.id) toast.success('Report schedule deleted') mutateReportSchedules() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to delete report schedule') } } const handleReportToggle = async (schedule: ReportSchedule) => { try { await updateReportSchedule(siteId, schedule.id, { enabled: !schedule.enabled }) toast.success(schedule.enabled ? 'Report paused' : 'Report enabled') mutateReportSchedules() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to update report schedule') } } const handleReportTest = async (schedule: ReportSchedule) => { setReportTesting(schedule.id) try { await testReportSchedule(siteId, schedule.id) toast.success('Test report sent successfully') } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to send test report') } finally { setReportTesting(null) } } // Alert channel handlers const resetAlertForm = () => { setAlertForm({ channel: 'email', recipients: '', webhookUrl: '', }) } const openEditAlert = (schedule: ReportSchedule) => { setEditingAlert(schedule) const isEmail = schedule.channel === 'email' setAlertForm({ channel: schedule.channel, recipients: isEmail ? (schedule.channel_config as EmailConfig).recipients.join(', ') : '', webhookUrl: !isEmail ? (schedule.channel_config as WebhookConfig).url : '', }) setAlertModalOpen(true) } const handleAlertSubmit = async (e: React.FormEvent) => { e.preventDefault() let channelConfig: EmailConfig | WebhookConfig if (alertForm.channel === 'email') { const recipients = alertForm.recipients.split(',').map(r => r.trim()).filter(r => r.length > 0) if (recipients.length === 0) { toast.error('At least one recipient email is required') return } channelConfig = { recipients } } else { if (!alertForm.webhookUrl.trim()) { toast.error('Webhook URL is required') return } channelConfig = { url: alertForm.webhookUrl.trim() } } const payload: CreateReportScheduleRequest = { channel: alertForm.channel, channel_config: channelConfig, frequency: 'daily', purpose: 'alert', } setAlertSaving(true) try { if (editingAlert) { await updateReportSchedule(siteId, editingAlert.id, { ...payload, purpose: 'alert' }) toast.success('Alert channel updated') } else { await createReportSchedule(siteId, payload) toast.success('Alert channel created') } setAlertModalOpen(false) mutateAlertSchedules() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to save alert channel') } finally { setAlertSaving(false) } } const handleAlertDelete = async (schedule: ReportSchedule) => { if (!confirm('Delete this alert channel?')) return try { await deleteReportSchedule(siteId, schedule.id) toast.success('Alert channel deleted') mutateAlertSchedules() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to delete alert channel') } } const handleAlertToggle = async (schedule: ReportSchedule) => { try { await updateReportSchedule(siteId, schedule.id, { enabled: !schedule.enabled }) toast.success(schedule.enabled ? 'Alert paused' : 'Alert enabled') mutateAlertSchedules() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to update alert channel') } } const handleAlertTest = async (schedule: ReportSchedule) => { setAlertTesting(schedule.id) try { await testReportSchedule(siteId, schedule.id) toast.success('Test alert sent successfully') } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to send test alert') } finally { setAlertTesting(null) } } const getChannelLabel = (channel: string) => { switch (channel) { case 'email': return 'Email' case 'slack': return 'Slack' case 'discord': return 'Discord' case 'webhook': return 'Webhook' default: return channel } } const getFrequencyLabel = (frequency: string) => { switch (frequency) { case 'daily': return 'Daily' case 'weekly': return 'Weekly' case 'monthly': return 'Monthly' default: return frequency } } const WEEKDAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] const formatHour = (hour: number) => { if (hour === 0) return '12:00 AM' if (hour === 12) return '12:00 PM' return hour < 12 ? `${hour}:00 AM` : `${hour - 12}:00 PM` } const getScheduleDescription = (schedule: ReportSchedule) => { const hour = formatHour(schedule.send_hour ?? 9) const tz = schedule.timezone || 'UTC' switch (schedule.frequency) { case 'daily': return `Every day at ${hour} (${tz})` case 'weekly': { const day = WEEKDAY_NAMES[schedule.send_day ?? 0] || 'Monday' return `Every ${day} at ${hour} (${tz})` } case 'monthly': { const d = schedule.send_day ?? 1 const suffix = d === 1 ? 'st' : d === 2 ? 'nd' : d === 3 ? 'rd' : 'th' return `${d}${suffix} of each month at ${hour} (${tz})` } default: return schedule.frequency } } const getReportTypeLabel = (type: string) => { switch (type) { case 'summary': return 'Summary' case 'pages': return 'Pages' case 'sources': return 'Sources' case 'goals': return 'Goals' default: return type } } const openAddGoal = () => { setEditingGoal(null) setGoalForm({ name: '', event_name: '' }) setGoalModalOpen(true) } const openEditGoal = (goal: Goal) => { setEditingGoal(goal) setGoalForm({ name: goal.name, event_name: goal.event_name }) setGoalModalOpen(true) } const handleGoalSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!goalForm.name.trim() || !goalForm.event_name.trim()) { toast.error('Name and event name are required') return } const eventName = goalForm.event_name.trim().toLowerCase().replace(/\s+/g, '_') if (eventName.length > 64) { toast.error('Event name must be 64 characters or less') return } if (!/^[a-zA-Z0-9_]+$/.test(eventName)) { toast.error('Event name can only contain letters, numbers, and underscores') return } const duplicateEventName = editingGoal ? goals.some((g) => g.id !== editingGoal.id && g.event_name === eventName) : goals.some((g) => g.event_name === eventName) if (duplicateEventName) { toast.error('A goal with this event name already exists') return } setGoalSaving(true) try { if (editingGoal) { await updateGoal(siteId, editingGoal.id, { name: goalForm.name.trim(), event_name: eventName }) toast.success('Goal updated') } else { await createGoal(siteId, { name: goalForm.name.trim(), event_name: eventName }) toast.success('Goal created') } setGoalModalOpen(false) mutateGoals() } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to save goal') } finally { setGoalSaving(false) } } const handleDeleteGoal = async (goal: Goal) => { if (!confirm(`Delete goal "${goal.name}"?`)) return try { await deleteGoal(siteId, goal.id) toast.success('Goal deleted') mutateGoals() } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to delete goal') } } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setSaving(true) try { const excludedPathsArray = formData.excluded_paths .split('\n') .map(p => p.trim()) .filter(p => p.length > 0) await updateSite(siteId, { name: formData.name, timezone: formData.timezone, is_public: formData.is_public, password: isPasswordEnabled ? (formData.password || undefined) : undefined, clear_password: !isPasswordEnabled, excluded_paths: excludedPathsArray, // Data collection settings collect_page_paths: formData.collect_page_paths, collect_referrers: formData.collect_referrers, collect_device_info: formData.collect_device_info, collect_geo_data: formData.collect_geo_data, collect_screen_resolution: formData.collect_screen_resolution, // Bot and noise filtering filter_bots: formData.filter_bots, // Hide unknown locations hide_unknown_locations: formData.hide_unknown_locations, // Data retention data_retention_months: formData.data_retention_months }) toast.success('Site updated successfully') initialFormRef.current = JSON.stringify({ name: formData.name, timezone: formData.timezone, is_public: formData.is_public, excluded_paths: formData.excluded_paths, collect_page_paths: formData.collect_page_paths, collect_referrers: formData.collect_referrers, collect_device_info: formData.collect_device_info, collect_geo_data: formData.collect_geo_data, collect_screen_resolution: formData.collect_screen_resolution, filter_bots: formData.filter_bots, hide_unknown_locations: formData.hide_unknown_locations, data_retention_months: formData.data_retention_months }) mutateSite() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to save site settings') } finally { setSaving(false) } } const handleResetData = async () => { if (!confirm('Are you sure you want to delete ALL data for this site? This action cannot be undone.')) { return } try { await resetSiteData(siteId) toast.success('All site data has been reset') } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to reset site data') } } const handleDeleteSite = () => { setShowDeleteModal(true) } const copyLink = () => { const link = `${APP_URL}/share/${siteId}` navigator.clipboard.writeText(link) setLinkCopied(true) toast.success('Link copied to clipboard') setTimeout(() => setLinkCopied(false), 2000) } const copySnippet = () => { if (!site) return navigator.clipboard.writeText(generatePrivacySnippet(site)) setSnippetCopied(true) toast.success('Privacy snippet copied to clipboard') setTimeout(() => setSnippetCopied(false), 2000) } const isFormDirty = initialFormRef.current !== '' && JSON.stringify({ name: formData.name, timezone: formData.timezone, is_public: formData.is_public, excluded_paths: formData.excluded_paths, collect_page_paths: formData.collect_page_paths, collect_referrers: formData.collect_referrers, collect_device_info: formData.collect_device_info, collect_geo_data: formData.collect_geo_data, collect_screen_resolution: formData.collect_screen_resolution, filter_bots: formData.filter_bots, hide_unknown_locations: formData.hide_unknown_locations, data_retention_months: formData.data_retention_months }) !== initialFormRef.current useUnsavedChanges(isFormDirty) useEffect(() => { if (site?.domain) document.title = `Settings · ${site.domain} | Pulse` }, [site?.domain]) // Handle GSC OAuth callback query params useEffect(() => { const gsc = searchParams.get('gsc') if (!gsc) return switch (gsc) { case 'connected': toast.success('Google Search Console connected successfully') mutateGSCStatus() break case 'denied': toast.error('Google authorization was denied') break case 'no_property': toast.error('No matching Search Console property found for this site') break case 'error': toast.error('Failed to connect Google Search Console') break } setActiveTab('integrations') window.history.replaceState({}, '', window.location.pathname) }, [searchParams, mutateGSCStatus]) const showSkeleton = useMinimumLoading(siteLoading && !site) const fadeClass = useSkeletonFade(showSkeleton) if (showSkeleton) { return (
) } if (!site) { return (

Site not found

) } return (

Site Settings

Manage settings for {site.domain}

{/* Sidebar Navigation */} {/* Content Area */}
{!canEdit && (

You have read-only access to this site. Contact an admin to make changes.

)} {activeTab === 'general' && (

General Configuration

Update your site details and tracking script.

setFormData({ ...formData, name: e.target.value })} className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900 focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white" /> {formData.name.length > 80 && ( 90 ? 'text-amber-500' : 'text-neutral-400'}`}>{formData.name.length}/100 )}

Domain cannot be changed after creation

Tracking Script

Add this script to your website to start tracking visitors. Choose your framework for setup instructions.

{ try { await updateSite(siteId, { name: site.name, script_features: features }) mutateSite() } catch { /* silent — not critical */ } }} />

{site.is_verified ? 'Your site is sending data correctly.' : 'Check if your site is sending data correctly.'}

{canEdit && ( )}
{canEdit && (

Danger Zone

Irreversible actions for your site.

Reset Data

Delete all stats and events. This cannot be undone.

Delete Site

Schedule this site for deletion with a 7-day grace period.

)}
)} {activeTab === 'visibility' && (

Visibility Settings

Manage who can view your dashboard.

Public Dashboard

Allow anyone with the link to view this dashboard

{formData.is_public && (

Share this link with others to view the dashboard.

Password Protection

Restrict access to this dashboard.

{isPasswordEnabled && site?.has_password && !formData.password && ( Password set )}
{isPasswordEnabled && (
setFormData({ ...formData, password: e.target.value })} placeholder="Enter new password" /> {site?.has_password && (

Current password will remain unchanged unless you enter a new one.

)} {site?.has_password && ( )} {!site?.has_password && (

Visitors will need to enter this password to view the dashboard.

)}
)}
)}
{canEdit && ( )}
)} {activeTab === 'data' && (

Data & Privacy

Control what visitor data is collected. Less data = more privacy.

{/* Data Collection Controls */}

Data Collection

{/* Page Paths Toggle */}

Page Paths

Track which pages visitors view

{/* Referrers Toggle */}

Referrers

Track where visitors come from

{/* Device Info Toggle */}

Device Info

Track browser, OS, and device type

{/* Geographic Data Dropdown */}

Geographic Data

Control location tracking granularity

setFormData({ ...formData, collect_screen_resolution: e.target.checked })} className="sr-only peer" />
{/* Filtering */}

Filtering

Hide unknown locations

Exclude entries where geographic data could not be resolved from location stats

{/* Data Retention */}

Data Retention

{!!subscriptionError && (

Plan limits could not be loaded. Options shown may be limited.

)}

Keep raw event data for

Events older than this are automatically deleted. Aggregated daily stats are kept permanently.

{ try { await updatePageSpeedConfig(siteId, { enabled: true, frequency: v }) mutatePSIConfig() toast.success(`PageSpeed frequency updated to ${v}`) } catch { toast.error('Failed to update frequency') } }} options={[ { value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' }, { value: 'monthly', label: 'Monthly' }, ]} variant="input" align="right" className="min-w-[130px]" /> ) : ( Not enabled )}
{/* Excluded Paths */}

Path Filtering