WIP: lokalen Stand vor dem Aufräumen sichern
Build & Deploy / Build & Push Docker Images (push) Has been cancelled
Docker Integration / Docker Compose Build (push) Has been cancelled
Frontend CI / Lint, Test & Build (20) (push) Has been cancelled
Frontend CI / Lint, Test & Build (22) (push) Has been cancelled
Build & Deploy / Deploy on Server (push) Has been cancelled
Build & Deploy / Build & Push Docker Images (push) Has been cancelled
Docker Integration / Docker Compose Build (push) Has been cancelled
Frontend CI / Lint, Test & Build (20) (push) Has been cancelled
Frontend CI / Lint, Test & Build (22) (push) Has been cancelled
Build & Deploy / Deploy on Server (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils";
|
import { logError, extractSupabaseErrorInfo, isMissingSchemaError } from "@/utils/errorUtils";
|
||||||
|
|
||||||
interface Alert {
|
interface Alert {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -48,6 +48,17 @@ interface AlertRule {
|
|||||||
created_at: string;
|
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() {
|
export default function AlertsPage() {
|
||||||
const { userDetails } = useAuth();
|
const { userDetails } = useAuth();
|
||||||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||||
@@ -73,7 +84,6 @@ export default function AlertsPage() {
|
|||||||
.from("alerts")
|
.from("alerts")
|
||||||
.select(`
|
.select(`
|
||||||
id,
|
id,
|
||||||
type,
|
|
||||||
severity,
|
severity,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
@@ -81,6 +91,7 @@ export default function AlertsPage() {
|
|||||||
created_at,
|
created_at,
|
||||||
resolved_at,
|
resolved_at,
|
||||||
acknowledged_at,
|
acknowledged_at,
|
||||||
|
details,
|
||||||
websites!inner (
|
websites!inner (
|
||||||
name,
|
name,
|
||||||
base_url,
|
base_url,
|
||||||
@@ -95,13 +106,13 @@ export default function AlertsPage() {
|
|||||||
|
|
||||||
const formattedAlerts: Alert[] = alertsData?.map((alert: any) => ({
|
const formattedAlerts: Alert[] = alertsData?.map((alert: any) => ({
|
||||||
id: alert.id,
|
id: alert.id,
|
||||||
type: alert.type,
|
type: (alert.details?.type as Alert["type"]) || "error",
|
||||||
severity: alert.severity,
|
severity: alert.severity,
|
||||||
title: alert.title,
|
title: alert.title,
|
||||||
message: alert.message,
|
message: alert.message,
|
||||||
website_name: alert.websites.name,
|
website_name: alert.websites.name,
|
||||||
website_url: alert.websites.base_url,
|
website_url: alert.websites.base_url,
|
||||||
status: alert.status,
|
status: alert.status === "open" ? "active" : alert.status,
|
||||||
created_at: alert.created_at,
|
created_at: alert.created_at,
|
||||||
resolved_at: alert.resolved_at,
|
resolved_at: alert.resolved_at,
|
||||||
acknowledged_at: alert.acknowledged_at,
|
acknowledged_at: alert.acknowledged_at,
|
||||||
@@ -109,15 +120,51 @@ export default function AlertsPage() {
|
|||||||
|
|
||||||
setAlerts(formattedAlerts);
|
setAlerts(formattedAlerts);
|
||||||
|
|
||||||
// Load alert rules
|
// Load alert rules (legacy table), fallback to alert_configurations for current schema.
|
||||||
const { data: rulesData, error: rulesError } = await supabase
|
const { data: rulesData, error: rulesError } = await supabase
|
||||||
.from("alert_rules")
|
.from("alert_rules")
|
||||||
.select("*")
|
.select("*")
|
||||||
.eq("organization_id", userDetails.organization_id)
|
.eq("organization_id", userDetails.organization_id)
|
||||||
.order("created_at", { ascending: false });
|
.order("created_at", { ascending: false });
|
||||||
|
|
||||||
if (rulesError) throw rulesError;
|
if (!rulesError) {
|
||||||
setAlertRules(rulesData || []);
|
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) {
|
} catch (error) {
|
||||||
const errorInfo = extractSupabaseErrorInfo(error);
|
const errorInfo = extractSupabaseErrorInfo(error);
|
||||||
@@ -162,11 +209,19 @@ export default function AlertsPage() {
|
|||||||
|
|
||||||
const toggleAlertRule = async (ruleId: string, enabled: boolean) => {
|
const toggleAlertRule = async (ruleId: string, enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
let { error } = await supabase
|
||||||
.from("alert_rules")
|
.from("alert_rules")
|
||||||
.update({ enabled: !enabled })
|
.update({ enabled: !enabled })
|
||||||
.eq("id", ruleId);
|
.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;
|
if (error) throw error;
|
||||||
|
|
||||||
setAlertRules(prev => prev.map(rule =>
|
setAlertRules(prev => prev.map(rule =>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils";
|
import { logError, extractSupabaseErrorInfo, isMissingSchemaError } from "@/utils/errorUtils";
|
||||||
|
|
||||||
interface MonitoringStatus {
|
interface MonitoringStatus {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -66,32 +66,43 @@ export default function MonitoringPage() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
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
|
const { data: websitesData, error: websitesError } = await supabase
|
||||||
.from("websites")
|
.from("websites")
|
||||||
.select(`
|
.select("id, name, base_url, is_active, created_at")
|
||||||
id,
|
|
||||||
name,
|
|
||||||
base_url,
|
|
||||||
is_active,
|
|
||||||
created_at,
|
|
||||||
uptime_checks (
|
|
||||||
id,
|
|
||||||
status,
|
|
||||||
response_time,
|
|
||||||
checked_at,
|
|
||||||
error_message
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.eq("organization_id", userDetails.organization_id)
|
.eq("organization_id", userDetails.organization_id)
|
||||||
.eq("is_active", true)
|
.eq("is_active", true)
|
||||||
.order("created_at", { ascending: false });
|
.order("created_at", { ascending: false });
|
||||||
|
|
||||||
if (websitesError) throw websitesError;
|
if (websitesError) throw websitesError;
|
||||||
|
|
||||||
|
const websiteIds = (websitesData || []).map((website: any) => website.id);
|
||||||
|
const checksByWebsite = new Map<string, any[]>();
|
||||||
|
|
||||||
|
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
|
// Process monitoring data
|
||||||
const monitoringData: MonitoringStatus[] = websitesData?.map((website: any) => {
|
const monitoringData: MonitoringStatus[] = websitesData?.map((website: any) => {
|
||||||
const checks = website.uptime_checks || [];
|
const checks = checksByWebsite.get(website.id) || [];
|
||||||
const recentChecks = checks
|
const recentChecks = checks
|
||||||
.filter((check: any) => {
|
.filter((check: any) => {
|
||||||
const checkDate = new Date(check.checked_at);
|
const checkDate = new Date(check.checked_at);
|
||||||
@@ -125,7 +136,7 @@ export default function MonitoringPage() {
|
|||||||
// Get recent checks for the timeline
|
// Get recent checks for the timeline
|
||||||
const allChecks: UptimeMetric[] = [];
|
const allChecks: UptimeMetric[] = [];
|
||||||
websitesData?.forEach((website: any) => {
|
websitesData?.forEach((website: any) => {
|
||||||
const checks = website.uptime_checks || [];
|
const checks = checksByWebsite.get(website.id) || [];
|
||||||
checks.slice(0, 10).forEach((check: any) => {
|
checks.slice(0, 10).forEach((check: any) => {
|
||||||
allChecks.push({
|
allChecks.push({
|
||||||
website_id: website.id,
|
website_id: website.id,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils";
|
import { logError, extractSupabaseErrorInfo, isMissingSchemaError } from "@/utils/errorUtils";
|
||||||
|
|
||||||
interface PerformanceMetric {
|
interface PerformanceMetric {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -79,12 +79,11 @@ export default function PerformancePage() {
|
|||||||
.from("scans")
|
.from("scans")
|
||||||
.select(`
|
.select(`
|
||||||
id,
|
id,
|
||||||
lighthouse_score,
|
|
||||||
created_at,
|
created_at,
|
||||||
scan_results!inner (
|
scan_results!inner (
|
||||||
category,
|
category,
|
||||||
score,
|
score,
|
||||||
metrics
|
raw_data
|
||||||
),
|
),
|
||||||
pages!inner (
|
pages!inner (
|
||||||
websites!inner (
|
websites!inner (
|
||||||
@@ -101,16 +100,7 @@ export default function PerformancePage() {
|
|||||||
.order("created_at", { ascending: false });
|
.order("created_at", { ascending: false });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const errorInfo = extractSupabaseErrorInfo(error);
|
if (isMissingSchemaError(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")) {
|
|
||||||
setMetrics([]);
|
setMetrics([]);
|
||||||
setSummary({
|
setSummary({
|
||||||
totalWebsites: 0,
|
totalWebsites: 0,
|
||||||
@@ -132,19 +122,37 @@ export default function PerformancePage() {
|
|||||||
if (!websiteMetrics.has(website.id)) {
|
if (!websiteMetrics.has(website.id)) {
|
||||||
const results = scan.scan_results || [];
|
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, {
|
websiteMetrics.set(website.id, {
|
||||||
id: scan.id,
|
id: scan.id,
|
||||||
website_name: website.name,
|
website_name: website.name,
|
||||||
website_url: website.base_url,
|
website_url: website.base_url,
|
||||||
lighthouse_score: scan.lighthouse_score || 0,
|
lighthouse_score: lighthouseScore,
|
||||||
performance_score: results.find((r: any) => r.category === "performance")?.score || 0,
|
performance_score: performanceResult?.score || 0,
|
||||||
accessibility_score: results.find((r: any) => r.category === "accessibility")?.score || 0,
|
accessibility_score: accessibilityResult?.score || 0,
|
||||||
best_practices_score: results.find((r: any) => r.category === "best-practices")?.score || 0,
|
best_practices_score: bestPracticesResult?.score || 0,
|
||||||
seo_score: results.find((r: any) => r.category === "seo")?.score || 0,
|
seo_score: seoResult?.score || 0,
|
||||||
first_contentful_paint: results.find((r: any) => r.category === "performance")?.metrics?.first_contentful_paint || 0,
|
first_contentful_paint: Number(perfRaw.firstContentfulPaint ?? perfRaw.first_contentful_paint ?? 0),
|
||||||
largest_contentful_paint: results.find((r: any) => r.category === "performance")?.metrics?.largest_contentful_paint || 0,
|
largest_contentful_paint: Number(perfRaw.largestContentfulPaint ?? perfRaw.largest_contentful_paint ?? 0),
|
||||||
cumulative_layout_shift: results.find((r: any) => r.category === "performance")?.metrics?.cumulative_layout_shift || 0,
|
cumulative_layout_shift: Number(perfRaw.cumulativeLayoutShift ?? perfRaw.cumulative_layout_shift ?? 0),
|
||||||
total_blocking_time: results.find((r: any) => r.category === "performance")?.metrics?.total_blocking_time || 0,
|
total_blocking_time: Number(perfRaw.totalBlockingTime ?? perfRaw.total_blocking_time ?? 0),
|
||||||
created_at: scan.created_at,
|
created_at: scan.created_at,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils";
|
import { logError, extractSupabaseErrorInfo, isMissingSchemaError } from "@/utils/errorUtils";
|
||||||
|
|
||||||
interface SEOMetric {
|
interface SEOMetric {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -80,12 +80,11 @@ export default function SEOPage() {
|
|||||||
.from("scans")
|
.from("scans")
|
||||||
.select(`
|
.select(`
|
||||||
id,
|
id,
|
||||||
lighthouse_score,
|
|
||||||
created_at,
|
created_at,
|
||||||
scan_results!inner (
|
scan_results!inner (
|
||||||
category,
|
category,
|
||||||
score,
|
score,
|
||||||
details
|
raw_data
|
||||||
),
|
),
|
||||||
pages!inner (
|
pages!inner (
|
||||||
id,
|
id,
|
||||||
@@ -103,7 +102,20 @@ export default function SEOPage() {
|
|||||||
.gte("created_at", startDate.toISOString())
|
.gte("created_at", startDate.toISOString())
|
||||||
.order("created_at", { ascending: false });
|
.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
|
// Process SEO data
|
||||||
const pageMetrics = new Map<string, SEOMetric>();
|
const pageMetrics = new Map<string, SEOMetric>();
|
||||||
@@ -115,21 +127,21 @@ export default function SEOPage() {
|
|||||||
const performanceResult = scan.scan_results?.find((r: any) => r.category === "performance");
|
const performanceResult = scan.scan_results?.find((r: any) => r.category === "performance");
|
||||||
|
|
||||||
if (!pageMetrics.has(page.id) && seoResult) {
|
if (!pageMetrics.has(page.id) && seoResult) {
|
||||||
const details = seoResult.details || {};
|
const details = seoResult.raw_data || {};
|
||||||
|
|
||||||
pageMetrics.set(page.id, {
|
pageMetrics.set(page.id, {
|
||||||
id: scan.id,
|
id: scan.id,
|
||||||
website_name: website.name,
|
website_name: website.name,
|
||||||
website_url: page.url || website.base_url,
|
website_url: page.url || website.base_url,
|
||||||
seo_score: seoResult.score || 0,
|
seo_score: seoResult.score || 0,
|
||||||
title_tag: details.has_title_tag || false,
|
title_tag: Boolean(details.has_title_tag ?? details.titleTag ?? false),
|
||||||
meta_description: details.has_meta_description || false,
|
meta_description: Boolean(details.has_meta_description ?? details.metaDescription ?? false),
|
||||||
h1_tag: details.has_h1_tag || false,
|
h1_tag: Boolean(details.has_h1_tag ?? details.h1Tag ?? false),
|
||||||
image_alt_text: details.images_with_alt || 0,
|
image_alt_text: Number(details.images_with_alt ?? details.imagesWithAlt ?? 0),
|
||||||
internal_links: details.internal_links || 0,
|
internal_links: Number(details.internal_links ?? details.internalLinks ?? 0),
|
||||||
external_links: details.external_links || 0,
|
external_links: Number(details.external_links ?? details.externalLinks ?? 0),
|
||||||
page_speed_score: performanceResult?.score || 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,
|
ssl_certificate: website.base_url?.startsWith("https://") || false,
|
||||||
created_at: scan.created_at,
|
created_at: scan.created_at,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
* Utility functions for proper error handling and logging
|
* Utility functions for proper error handling and logging
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const ERROR_DEDUP_WINDOW_MS = 5000;
|
||||||
|
const MAX_ERROR_SIGNATURES = 200;
|
||||||
|
const recentErrorSignatures = new Map<string, number>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely serialize an error object to avoid circular references
|
* Safely serialize an error object to avoid circular references
|
||||||
* and ensure all properties are captured
|
* and ensure all properties are captured
|
||||||
@@ -92,7 +96,6 @@ export function serializeError(error: unknown): string {
|
|||||||
* Enhanced console.error that properly serializes errors
|
* Enhanced console.error that properly serializes errors
|
||||||
*/
|
*/
|
||||||
export function logError(message: string, error: unknown, context?: Record<string, any>): void {
|
export function logError(message: string, error: unknown, context?: Record<string, any>): void {
|
||||||
// Try to extract more information from the error
|
|
||||||
let errorDetails = serializeError(error);
|
let errorDetails = serializeError(error);
|
||||||
|
|
||||||
// If the error is still showing as an object, try to extract more info
|
// If the error is still showing as an object, try to extract more info
|
||||||
@@ -126,6 +129,29 @@ export function logError(message: string, error: unknown, context?: Record<strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errorSummary = error instanceof Error
|
||||||
|
? `${error.name}: ${error.message}`
|
||||||
|
: error && typeof error === 'object' && 'message' in error
|
||||||
|
? String((error as Record<string, unknown>).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 = {
|
const errorInfo = {
|
||||||
message,
|
message,
|
||||||
error: errorDetails,
|
error: errorDetails,
|
||||||
@@ -135,16 +161,7 @@ export function logError(message: string, error: unknown, context?: Record<strin
|
|||||||
errorType: error?.constructor?.name || typeof error,
|
errorType: error?.constructor?.name || typeof error,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.error('🚨 Error:', errorInfo);
|
console.error('Error:', errorInfo);
|
||||||
|
|
||||||
// In development, also log the original error for better debugging
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.error('Original error:', error);
|
|
||||||
if (error && typeof error === 'object') {
|
|
||||||
console.error('Error keys:', Object.keys(error));
|
|
||||||
console.error('Error prototype:', Object.getPrototypeOf(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,6 +244,24 @@ export function isAuthError(error: unknown): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an error indicates missing tables, columns, or relationships
|
||||||
|
* due to schema mismatches.
|
||||||
|
*/
|
||||||
|
export function isMissingSchemaError(error: unknown): boolean {
|
||||||
|
const info = extractSupabaseErrorInfo(error);
|
||||||
|
const combined = `${info.message || ''} ${info.details || ''}`.toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
info.code === 'PGRST200' || // missing relationship in schema cache
|
||||||
|
info.code === 'PGRST205' || // table not found in schema cache
|
||||||
|
info.code === '42P01' || // relation does not exist
|
||||||
|
info.code === '42703' || // column does not exist
|
||||||
|
combined.includes('does not exist') ||
|
||||||
|
combined.includes('could not find a relationship')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract detailed information from Supabase errors
|
* Extract detailed information from Supabase errors
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Fix processed_urls column type: was incorrectly added as TEXT[] in a previous migration,
|
||||||
|
-- but should be INTEGER to match API usage and other integer columns (total_urls, pages_processed, etc.)
|
||||||
|
ALTER TABLE crawl_sessions
|
||||||
|
ALTER COLUMN processed_urls TYPE INTEGER USING 0;
|
||||||
|
|
||||||
|
-- Refresh schema cache
|
||||||
|
NOTIFY pgrst, 'reload schema';
|
||||||
Reference in New Issue
Block a user