diff --git a/frontend/src/app/dashboard/alerts/page.tsx b/frontend/src/app/dashboard/alerts/page.tsx index efd36ed..fecbee8 100644 --- a/frontend/src/app/dashboard/alerts/page.tsx +++ b/frontend/src/app/dashboard/alerts/page.tsx @@ -21,7 +21,7 @@ import { } from "lucide-react"; import { useAuth } from "@/contexts/AuthContext"; import { supabase } from "@/lib/supabase"; -import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils"; +import { logError, extractSupabaseErrorInfo, isMissingSchemaError } from "@/utils/errorUtils"; interface Alert { id: string; @@ -48,6 +48,17 @@ interface AlertRule { created_at: string; } +type AlertConfigurationRow = { + id: string; + name: string; + threshold: number | null; + comparison: string | null; + is_active: boolean | null; + notification_channels: string[] | null; + created_at: string; + metric_id?: string | null; +}; + export default function AlertsPage() { const { userDetails } = useAuth(); const [alerts, setAlerts] = useState([]); @@ -73,7 +84,6 @@ export default function AlertsPage() { .from("alerts") .select(` id, - type, severity, title, message, @@ -81,6 +91,7 @@ export default function AlertsPage() { created_at, resolved_at, acknowledged_at, + details, websites!inner ( name, base_url, @@ -95,13 +106,13 @@ export default function AlertsPage() { const formattedAlerts: Alert[] = alertsData?.map((alert: any) => ({ id: alert.id, - type: alert.type, + type: (alert.details?.type as Alert["type"]) || "error", severity: alert.severity, title: alert.title, message: alert.message, website_name: alert.websites.name, website_url: alert.websites.base_url, - status: alert.status, + status: alert.status === "open" ? "active" : alert.status, created_at: alert.created_at, resolved_at: alert.resolved_at, acknowledged_at: alert.acknowledged_at, @@ -109,15 +120,51 @@ export default function AlertsPage() { setAlerts(formattedAlerts); - // Load alert rules + // Load alert rules (legacy table), fallback to alert_configurations for current schema. const { data: rulesData, error: rulesError } = await supabase .from("alert_rules") .select("*") .eq("organization_id", userDetails.organization_id) .order("created_at", { ascending: false }); - if (rulesError) throw rulesError; - setAlertRules(rulesData || []); + if (!rulesError) { + setAlertRules((rulesData || []) as AlertRule[]); + } else if (isMissingSchemaError(rulesError)) { + const { data: configsData, error: configsError } = await supabase + .from("alert_configurations") + .select(` + id, + name, + threshold, + comparison, + is_active, + notification_channels, + created_at, + metric_id, + websites!inner(organization_id) + `) + .eq("websites.organization_id", userDetails.organization_id) + .order("created_at", { ascending: false }); + + if (configsError) throw configsError; + + const mappedRules: AlertRule[] = (configsData || []).map((row: any) => { + const config = row as AlertConfigurationRow; + return { + id: config.id, + name: config.name, + type: "performance", + condition: config.comparison || "less_than", + threshold: Number(config.threshold ?? 0), + enabled: Boolean(config.is_active), + notification_methods: config.notification_channels || ["email"], + created_at: config.created_at, + }; + }); + setAlertRules(mappedRules); + } else { + throw rulesError; + } } catch (error) { const errorInfo = extractSupabaseErrorInfo(error); @@ -162,11 +209,19 @@ export default function AlertsPage() { const toggleAlertRule = async (ruleId: string, enabled: boolean) => { try { - const { error } = await supabase + let { error } = await supabase .from("alert_rules") .update({ enabled: !enabled }) .eq("id", ruleId); + if (error && isMissingSchemaError(error)) { + const fallbackResult = await supabase + .from("alert_configurations") + .update({ is_active: !enabled }) + .eq("id", ruleId); + error = fallbackResult.error; + } + if (error) throw error; setAlertRules(prev => prev.map(rule => @@ -491,4 +546,4 @@ export default function AlertsPage() { ); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/monitoring/page.tsx b/frontend/src/app/dashboard/monitoring/page.tsx index 487a4ad..389173f 100644 --- a/frontend/src/app/dashboard/monitoring/page.tsx +++ b/frontend/src/app/dashboard/monitoring/page.tsx @@ -21,7 +21,7 @@ import { } from "lucide-react"; import { useAuth } from "@/contexts/AuthContext"; import { supabase } from "@/lib/supabase"; -import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils"; +import { logError, extractSupabaseErrorInfo, isMissingSchemaError } from "@/utils/errorUtils"; interface MonitoringStatus { id: string; @@ -66,32 +66,43 @@ export default function MonitoringPage() { try { setLoading(true); - // Fetch websites with monitoring status + // Fetch websites first; uptime checks are optional because some schemas + // do not include the uptime_checks table yet. const { data: websitesData, error: websitesError } = await supabase .from("websites") - .select(` - id, - name, - base_url, - is_active, - created_at, - uptime_checks ( - id, - status, - response_time, - checked_at, - error_message - ) - `) + .select("id, name, base_url, is_active, created_at") .eq("organization_id", userDetails.organization_id) .eq("is_active", true) .order("created_at", { ascending: false }); if (websitesError) throw websitesError; + const websiteIds = (websitesData || []).map((website: any) => website.id); + const checksByWebsite = new Map(); + + if (websiteIds.length > 0) { + const { data: checksData, error: checksError } = await supabase + .from("uptime_checks") + .select("website_id, status, response_time, checked_at, error_message") + .in("website_id", websiteIds) + .order("checked_at", { ascending: false }); + + if (checksError) { + if (!isMissingSchemaError(checksError)) { + throw checksError; + } + } else { + (checksData || []).forEach((check: any) => { + const checks = checksByWebsite.get(check.website_id) || []; + checks.push(check); + checksByWebsite.set(check.website_id, checks); + }); + } + } + // Process monitoring data const monitoringData: MonitoringStatus[] = websitesData?.map((website: any) => { - const checks = website.uptime_checks || []; + const checks = checksByWebsite.get(website.id) || []; const recentChecks = checks .filter((check: any) => { const checkDate = new Date(check.checked_at); @@ -125,7 +136,7 @@ export default function MonitoringPage() { // Get recent checks for the timeline const allChecks: UptimeMetric[] = []; websitesData?.forEach((website: any) => { - const checks = website.uptime_checks || []; + const checks = checksByWebsite.get(website.id) || []; checks.slice(0, 10).forEach((check: any) => { allChecks.push({ website_id: website.id, @@ -428,4 +439,4 @@ export default function MonitoringPage() { ); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/performance/page.tsx b/frontend/src/app/dashboard/performance/page.tsx index c7d65da..b84ec86 100644 --- a/frontend/src/app/dashboard/performance/page.tsx +++ b/frontend/src/app/dashboard/performance/page.tsx @@ -18,7 +18,7 @@ import { } from "lucide-react"; import { useAuth } from "@/contexts/AuthContext"; import { supabase } from "@/lib/supabase"; -import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils"; +import { logError, extractSupabaseErrorInfo, isMissingSchemaError } from "@/utils/errorUtils"; interface PerformanceMetric { id: string; @@ -79,12 +79,11 @@ export default function PerformancePage() { .from("scans") .select(` id, - lighthouse_score, created_at, scan_results!inner ( category, score, - metrics + raw_data ), pages!inner ( websites!inner ( @@ -101,16 +100,7 @@ export default function PerformancePage() { .order("created_at", { ascending: false }); if (error) { - const errorInfo = extractSupabaseErrorInfo(error); - logError("Error loading performance data", error, { - organizationId: userDetails.organization_id, - timeRange, - startDate: startDate.toISOString(), - supabaseError: errorInfo - }); - - // If tables don't exist, set empty metrics - if (errorInfo.message?.includes("does not exist") || errorInfo.details?.includes("does not exist")) { + if (isMissingSchemaError(error)) { setMetrics([]); setSummary({ totalWebsites: 0, @@ -132,19 +122,37 @@ export default function PerformancePage() { if (!websiteMetrics.has(website.id)) { const results = scan.scan_results || []; + const performanceResult = results.find((r: any) => r.category === "performance"); + const accessibilityResult = results.find((r: any) => r.category === "accessibility"); + const seoResult = results.find((r: any) => r.category === "seo"); + const bestPracticesResult = results.find( + (r: any) => r.category === "best_practices" || r.category === "best-practices" + ); + const perfRaw = performanceResult?.raw_data || {}; + const categoryScores = [ + Number(performanceResult?.score ?? 0), + Number(accessibilityResult?.score ?? 0), + Number(bestPracticesResult?.score ?? 0), + Number(seoResult?.score ?? 0), + ].filter((score) => Number.isFinite(score) && score > 0); + const lighthouseScore = + categoryScores.length > 0 + ? Math.round(categoryScores.reduce((sum, score) => sum + score, 0) / categoryScores.length) + : 0; + websiteMetrics.set(website.id, { id: scan.id, website_name: website.name, website_url: website.base_url, - lighthouse_score: scan.lighthouse_score || 0, - performance_score: results.find((r: any) => r.category === "performance")?.score || 0, - accessibility_score: results.find((r: any) => r.category === "accessibility")?.score || 0, - best_practices_score: results.find((r: any) => r.category === "best-practices")?.score || 0, - seo_score: results.find((r: any) => r.category === "seo")?.score || 0, - first_contentful_paint: results.find((r: any) => r.category === "performance")?.metrics?.first_contentful_paint || 0, - largest_contentful_paint: results.find((r: any) => r.category === "performance")?.metrics?.largest_contentful_paint || 0, - cumulative_layout_shift: results.find((r: any) => r.category === "performance")?.metrics?.cumulative_layout_shift || 0, - total_blocking_time: results.find((r: any) => r.category === "performance")?.metrics?.total_blocking_time || 0, + lighthouse_score: lighthouseScore, + performance_score: performanceResult?.score || 0, + accessibility_score: accessibilityResult?.score || 0, + best_practices_score: bestPracticesResult?.score || 0, + seo_score: seoResult?.score || 0, + first_contentful_paint: Number(perfRaw.firstContentfulPaint ?? perfRaw.first_contentful_paint ?? 0), + largest_contentful_paint: Number(perfRaw.largestContentfulPaint ?? perfRaw.largest_contentful_paint ?? 0), + cumulative_layout_shift: Number(perfRaw.cumulativeLayoutShift ?? perfRaw.cumulative_layout_shift ?? 0), + total_blocking_time: Number(perfRaw.totalBlockingTime ?? perfRaw.total_blocking_time ?? 0), created_at: scan.created_at, }); } @@ -397,4 +405,4 @@ export default function PerformancePage() { ); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/seo/page.tsx b/frontend/src/app/dashboard/seo/page.tsx index dd6ee14..d6b74ff 100644 --- a/frontend/src/app/dashboard/seo/page.tsx +++ b/frontend/src/app/dashboard/seo/page.tsx @@ -19,7 +19,7 @@ import { } from "lucide-react"; import { useAuth } from "@/contexts/AuthContext"; import { supabase } from "@/lib/supabase"; -import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils"; +import { logError, extractSupabaseErrorInfo, isMissingSchemaError } from "@/utils/errorUtils"; interface SEOMetric { id: string; @@ -80,12 +80,11 @@ export default function SEOPage() { .from("scans") .select(` id, - lighthouse_score, created_at, scan_results!inner ( category, score, - details + raw_data ), pages!inner ( id, @@ -103,7 +102,20 @@ export default function SEOPage() { .gte("created_at", startDate.toISOString()) .order("created_at", { ascending: false }); - if (error) throw error; + if (error) { + if (isMissingSchemaError(error)) { + setMetrics([]); + setSummary({ + totalPages: 0, + averageSEOScore: 0, + goodSEO: 0, + needsImprovement: 0, + poor: 0, + }); + return; + } + throw error; + } // Process SEO data const pageMetrics = new Map(); @@ -115,21 +127,21 @@ export default function SEOPage() { const performanceResult = scan.scan_results?.find((r: any) => r.category === "performance"); if (!pageMetrics.has(page.id) && seoResult) { - const details = seoResult.details || {}; + const details = seoResult.raw_data || {}; pageMetrics.set(page.id, { id: scan.id, website_name: website.name, website_url: page.url || website.base_url, seo_score: seoResult.score || 0, - title_tag: details.has_title_tag || false, - meta_description: details.has_meta_description || false, - h1_tag: details.has_h1_tag || false, - image_alt_text: details.images_with_alt || 0, - internal_links: details.internal_links || 0, - external_links: details.external_links || 0, + title_tag: Boolean(details.has_title_tag ?? details.titleTag ?? false), + meta_description: Boolean(details.has_meta_description ?? details.metaDescription ?? false), + h1_tag: Boolean(details.has_h1_tag ?? details.h1Tag ?? false), + image_alt_text: Number(details.images_with_alt ?? details.imagesWithAlt ?? 0), + internal_links: Number(details.internal_links ?? details.internalLinks ?? 0), + external_links: Number(details.external_links ?? details.externalLinks ?? 0), page_speed_score: performanceResult?.score || 0, - mobile_friendly: details.mobile_friendly || false, + mobile_friendly: Boolean(details.mobile_friendly ?? details.mobileFriendly ?? false), ssl_certificate: website.base_url?.startsWith("https://") || false, created_at: scan.created_at, }); @@ -407,4 +419,4 @@ export default function SEOPage() { ); -} \ No newline at end of file +} diff --git a/frontend/src/utils/errorUtils.ts b/frontend/src/utils/errorUtils.ts index 614a2fc..adf6aee 100644 --- a/frontend/src/utils/errorUtils.ts +++ b/frontend/src/utils/errorUtils.ts @@ -2,6 +2,10 @@ * Utility functions for proper error handling and logging */ +const ERROR_DEDUP_WINDOW_MS = 5000; +const MAX_ERROR_SIGNATURES = 200; +const recentErrorSignatures = new Map(); + /** * Safely serialize an error object to avoid circular references * and ensure all properties are captured @@ -92,9 +96,8 @@ export function serializeError(error: unknown): string { * Enhanced console.error that properly serializes errors */ export function logError(message: string, error: unknown, context?: Record): void { - // Try to extract more information from the error let errorDetails = serializeError(error); - + // If the error is still showing as an object, try to extract more info if (errorDetails === '[Non-serializable object]' || errorDetails.includes('Object')) { try { @@ -125,7 +128,30 @@ export function logError(message: string, error: unknown, context?: Record).message) + : String(error); + const signature = `${message}|${errorSummary}|${context?.function || ''}`; + const now = Date.now(); + const lastSeenAt = recentErrorSignatures.get(signature); + + // Suppress duplicate logs within a short window to avoid console spam. + if (lastSeenAt && now - lastSeenAt < ERROR_DEDUP_WINDOW_MS) { + return; + } + + recentErrorSignatures.set(signature, now); + if (recentErrorSignatures.size > MAX_ERROR_SIGNATURES) { + recentErrorSignatures.forEach((timestamp, key) => { + if (now - timestamp > ERROR_DEDUP_WINDOW_MS) { + recentErrorSignatures.delete(key); + } + }); + } + const errorInfo = { message, error: errorDetails, @@ -134,17 +160,8 @@ export function logError(message: string, error: unknown, context?: Record