From cc3047edba9b8a05e6813a818345ea3d3ea49b97 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Thu, 26 Mar 2026 10:50:36 +0100 Subject: [PATCH] refactor: replace legacy settings pages with redirect handlers + delete unused files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /sites/:id/settings → redirect handler for GSC OAuth callback + deep links - /org-settings → redirect handler for tab deep links - Deleted: OrganizationSettings.tsx, SettingsModalWrapper.tsx, settings-modal-context.tsx --- app/org-settings/page.tsx | 56 +- app/sites/[id]/settings/page.tsx | 2681 +----------------- components/settings/OrganizationSettings.tsx | 1577 ---------- components/settings/SettingsModalWrapper.tsx | 124 - lib/settings-modal-context.tsx | 31 - 5 files changed, 91 insertions(+), 4378 deletions(-) delete mode 100644 components/settings/OrganizationSettings.tsx delete mode 100644 components/settings/SettingsModalWrapper.tsx delete mode 100644 lib/settings-modal-context.tsx diff --git a/app/org-settings/page.tsx b/app/org-settings/page.tsx index 2d7fdab..75633ae 100644 --- a/app/org-settings/page.tsx +++ b/app/org-settings/page.tsx @@ -1,30 +1,38 @@ -import { Suspense } from 'react' -import OrganizationSettings from '@/components/settings/OrganizationSettings' -import { SettingsFormSkeleton } from '@/components/skeletons' +'use client' -export const metadata = { - title: 'Organization Settings - Pulse', - description: 'Manage your organization settings', -} +import { useEffect } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { useUnifiedSettings } from '@/lib/unified-settings-context' +import { Spinner } from '@ciphera-net/ui' + +/** + * Legacy org settings page — now a redirect handler. + * Redirects to home and opens unified settings modal at the correct workspace tab. + */ +export default function OrgSettingsRedirect() { + const router = useRouter() + const searchParams = useSearchParams() + const { openUnifiedSettings } = useUnifiedSettings() + + useEffect(() => { + const tab = searchParams.get('tab') + + const tabMap: Record = { + general: 'general', + members: 'members', + billing: 'billing', + notifications: 'notifications', + audit: 'audit', + } + + const mappedTab = tab ? tabMap[tab] || 'general' : 'general' + router.replace('/') + setTimeout(() => openUnifiedSettings({ context: 'workspace', tab: mappedTab }), 100) + }, [searchParams, router, openUnifiedSettings]) -export default function OrgSettingsPage() { return ( -
-
- -
-
-
-
-
- -
-
- }> - - -
+
+
) } diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx index 6182b99..530a37b 100644 --- a/app/sites/[id]/settings/page.tsx +++ b/app/sites/[id]/settings/page.tsx @@ -1,2640 +1,77 @@ 'use client' -import { useEffect, useState, useRef } from 'react' +import { useEffect } 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' +import { toast } from '@ciphera-net/ui' +import { useUnifiedSettings } from '@/lib/unified-settings-context' +import { useGSCStatus } from '@/lib/swr/dashboard' +import { Spinner } from '@ciphera-net/ui' +/** + * Legacy settings page — now a redirect handler. + * + * The unified settings modal has replaced the full-page settings. + * This page only exists to handle: + * 1. GSC OAuth callbacks (?gsc=connected|denied|error|no_property) + * 2. Deep links with ?tab= params + * 3. Direct navigation — redirects to site dashboard and opens modal + */ +export default function SettingsRedirect() { 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 siteId = params.id as string + const { openUnifiedSettings } = useUnifiedSettings() + const { mutate: mutateGSCStatus } = useGSCStatus(siteId) - 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 + const tab = searchParams.get('tab') + + // Handle GSC OAuth callback + if (gsc) { + 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 'token_error': + case 'error': + toast.error('Failed to connect Google Search Console') + break + } + // Redirect to site page and open integrations tab + router.replace(`/sites/${siteId}`) + setTimeout(() => openUnifiedSettings({ context: 'site', tab: 'integrations' }), 100) + return } - setActiveTab('integrations') - window.history.replaceState({}, '', window.location.pathname) - }, [searchParams, mutateGSCStatus]) - const showSkeleton = useMinimumLoading(siteLoading && !site) - const fadeClass = useSkeletonFade(showSkeleton) + // Handle deep links with ?tab= param + const tabMap: Record = { + general: 'general', + visibility: 'visibility', + data: 'privacy', + privacy: 'privacy', + bot: 'bot-spam', + goals: 'goals', + notifications: 'reports', + integrations: 'integrations', + } - if (showSkeleton) { - return ( -
-
-
-
-
-
-
- -
- -
-
-
-
- ) - } - - if (!site) { - return ( -
-

Site not found

-
- ) - } + const mappedTab = tab ? tabMap[tab] || 'general' : 'general' + router.replace(`/sites/${siteId}`) + setTimeout(() => openUnifiedSettings({ context: 'site', tab: mappedTab }), 100) + }, [siteId, searchParams, router, openUnifiedSettings, mutateGSCStatus]) 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

-
- -
-