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";
|
||||
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<Alert[]>([]);
|
||||
@@ -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() {
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
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() {
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, SEOMetric>();
|
||||
@@ -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() {
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, number>();
|
||||
|
||||
/**
|
||||
* 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<string, any>): 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<strin
|
||||
errorDetails = `[Error object: ${error?.constructor?.name || 'Unknown'}]`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 = {
|
||||
message,
|
||||
error: errorDetails,
|
||||
@@ -134,17 +160,8 @@ export function logError(message: string, error: unknown, context?: Record<strin
|
||||
userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'server',
|
||||
errorType: error?.constructor?.name || typeof error,
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Error:', errorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,6 +244,24 @@ export function isAuthError(error: unknown): boolean {
|
||||
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
|
||||
*/
|
||||
@@ -275,4 +310,4 @@ export function extractSupabaseErrorInfo(error: unknown): {
|
||||
return {
|
||||
message: String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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