'use client' import { useAuth } from '@/lib/auth/context' import { useEffect, useState, useRef, useCallback } from 'react' import { useParams } from 'next/navigation' import { useSite, usePageSpeedConfig, usePageSpeedLatest, usePageSpeedHistory } from '@/lib/swr/dashboard' import { updatePageSpeedConfig, triggerPageSpeedCheck, type PageSpeedCheck, type AuditSummary } from '@/lib/api/pagespeed' import { toast, Button } from '@ciphera-net/ui' import ScoreGauge from '@/components/pagespeed/ScoreGauge' import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ReferenceLine, } from 'recharts' import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/charts' // * Chart configuration for score trend const chartConfig = { score: { label: 'Performance', color: 'var(--chart-1)' }, } satisfies ChartConfig // * Metric status thresholds (Google's Core Web Vitals thresholds) function getMetricStatus(metric: string, value: number | null): { label: string; color: string } { if (value === null) return { label: '--', color: 'text-neutral-400' } const thresholds: Record = { lcp: [2500, 4000], cls: [0.1, 0.25], tbt: [200, 600], fcp: [1800, 3000], si: [3400, 5800], tti: [3800, 7300], } const [good, poor] = thresholds[metric] ?? [0, 0] if (value <= good) return { label: 'Good', color: 'text-emerald-600 dark:text-emerald-400' } if (value <= poor) return { label: 'Needs Improvement', color: 'text-amber-600 dark:text-amber-400' } return { label: 'Poor', color: 'text-red-600 dark:text-red-400' } } // * Format metric values for display function formatMetricValue(metric: string, value: number | null): string { if (value === null) return '--' if (metric === 'cls') return value.toFixed(3) if (value < 1000) return `${value}ms` return `${(value / 1000).toFixed(1)}s` } // * Format time ago for last checked display function formatTimeAgo(dateString: string | null): string { if (!dateString) return 'Never' const date = new Date(dateString) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffSec = Math.floor(diffMs / 1000) if (diffSec < 60) return 'just now' if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago` if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago` return `${Math.floor(diffSec / 86400)}d ago` } // * Get dot color for audit items based on score function getAuditDotColor(score: number | null): string { if (score === null) return 'bg-neutral-400' if (score >= 0.9) return 'bg-emerald-500' if (score >= 0.5) return 'bg-amber-500' return 'bg-red-500' } // * Main PageSpeed page export default function PageSpeedPage() { const { user } = useAuth() const canEdit = user?.role === 'owner' || user?.role === 'admin' const params = useParams() const siteId = params.id as string const { data: site } = useSite(siteId) const { data: config, mutate: mutateConfig } = usePageSpeedConfig(siteId) const { data: latestChecks, isLoading, mutate: mutateLatest } = usePageSpeedLatest(siteId) const [strategy, setStrategy] = useState<'mobile' | 'desktop'>('mobile') const [running, setRunning] = useState(false) const [toggling, setToggling] = useState(false) const [frequency, setFrequency] = useState('weekly') const { data: historyChecks } = usePageSpeedHistory(siteId, strategy) // * Get the check for the current strategy const currentCheck = latestChecks?.find(c => c.strategy === strategy) ?? null // * Set document title useEffect(() => { if (site?.domain) document.title = `PageSpeed · ${site.domain} | Pulse` }, [site?.domain]) // * Sync frequency from config when loaded useEffect(() => { if (config?.frequency) setFrequency(config.frequency) }, [config?.frequency]) // * Toggle PageSpeed monitoring on/off const handleToggle = async (enabled: boolean) => { setToggling(true) try { await updatePageSpeedConfig(siteId, { enabled, frequency }) mutateConfig() mutateLatest() toast.success(enabled ? 'PageSpeed monitoring enabled' : 'PageSpeed monitoring disabled') } catch { toast.error('Failed to update PageSpeed monitoring') } finally { setToggling(false) } } // * Trigger a manual PageSpeed check // * Poll for results after triggering an async check const pollRef = useRef | null>(null) const stopPolling = useCallback(() => { if (pollRef.current) { clearInterval(pollRef.current) pollRef.current = null } }, []) // * Clean up polling on unmount useEffect(() => () => stopPolling(), [stopPolling]) const handleRunCheck = async () => { setRunning(true) try { await triggerPageSpeedCheck(siteId) toast.success('PageSpeed check started — results will appear in 30-60 seconds') // * Poll every 5s for up to 2 minutes until new results appear const startedAt = Date.now() const initialCheckedAt = latestChecks?.[0]?.checked_at stopPolling() pollRef.current = setInterval(async () => { const elapsed = Date.now() - startedAt if (elapsed > 120_000) { stopPolling() setRunning(false) toast.error('Check is taking longer than expected. Results will appear when ready.') return } const freshData = await mutateLatest() const freshCheckedAt = freshData?.[0]?.checked_at if (freshCheckedAt && freshCheckedAt !== initialCheckedAt) { stopPolling() setRunning(false) toast.success('PageSpeed check complete') } }, 5000) } catch (err: any) { toast.error(err?.message || 'Failed to start check') setRunning(false) } } // * Loading state if (isLoading && !latestChecks) return if (!site) return
Site not found
const enabled = config?.enabled ?? false // * Disabled state — show empty state with enable toggle if (!enabled) { return (
{/* Header */}

PageSpeed

Monitor your site's performance and Core Web Vitals

{/* Empty state */}

PageSpeed monitoring is disabled

Enable PageSpeed monitoring to track your site's performance scores, Core Web Vitals, and get actionable improvement suggestions.

{/* Frequency selector */}
{canEdit && ( )}
) } // * Prepare chart data from history const chartData = (historyChecks ?? []).map(c => ({ date: new Date(c.checked_at).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }), score: c.performance_score, })) // * Parse audits into groups const audits = currentCheck?.audits ?? [] const opportunities = audits .filter(a => a.category === 'opportunity') .sort((a, b) => (b.savings_ms ?? 0) - (a.savings_ms ?? 0)) const diagnostics = audits.filter(a => a.category === 'diagnostic') const passed = audits.filter(a => a.category === 'passed') // * Core Web Vitals metrics const metrics = [ { key: 'lcp', label: 'Largest Contentful Paint', value: currentCheck?.lcp_ms ?? null }, { key: 'cls', label: 'Cumulative Layout Shift', value: currentCheck?.cls ?? null }, { key: 'tbt', label: 'Total Blocking Time', value: currentCheck?.tbt_ms ?? null }, { key: 'fcp', label: 'First Contentful Paint', value: currentCheck?.fcp_ms ?? null }, { key: 'si', label: 'Speed Index', value: currentCheck?.si_ms ?? null }, { key: 'tti', label: 'Time to Interactive', value: currentCheck?.tti_ms ?? null }, ] // * Enabled state — show full PageSpeed dashboard return (
{/* Header */}

PageSpeed

Performance scores and Core Web Vitals for {site.domain}

{/* Mobile / Desktop toggle */}
{canEdit && ( <> )}
{/* Section 1 — Score Overview */}
{/* Last checked info */}
{currentCheck?.checked_at && ( Last checked {formatTimeAgo(currentCheck.checked_at)} )} {config?.frequency && ( {config.frequency} )}
{/* Section 2 — Core Web Vitals */}

Core Web Vitals

{metrics.map(({ key, label, value }) => { const status = getMetricStatus(key, value) return (
{label}
{formatMetricValue(key, value)}
{status.label}
) })}
{/* Section 3 — Score Trend Chart */} {chartData.length >= 2 && (

Performance Score Trend

{value}} /> } />
)} {/* Section 4 — Diagnostics Accordion */} {audits.length > 0 && (

Diagnostics

{/* Opportunities */} {opportunities.length > 0 && (
Opportunities ({opportunities.length})
{opportunities.map(audit => (
{audit.title} {audit.display_value && ( {audit.display_value} )}
))}
)} {/* Diagnostics */} {diagnostics.length > 0 && (
Diagnostics ({diagnostics.length})
{diagnostics.map(audit => (
{audit.title} {audit.display_value && ( {audit.display_value} )}
))}
)} {/* Passed Audits */} {passed.length > 0 && (
Passed Audits ({passed.length})
{passed.map(audit => (
{audit.title} {audit.display_value && ( {audit.display_value} )}
))}
)}
)}
) } // * Skeleton loading state function PageSpeedSkeleton() { return (
{[...Array(4)].map((_, i) => (
))}
{[...Array(6)].map((_, i) => (
))}
) }