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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dennis
2026-10-15 15:32:30 +02:00
parent 523aacb37a
commit dedf52b99d
6 changed files with 207 additions and 79 deletions
+64 -9
View File
@@ -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>
);
}
}
+30 -19
View File
@@ -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>
);
}
}
+31 -23
View File
@@ -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>
);
}
}
+25 -13
View File
@@ -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>
);
}
}
+50 -15
View File
@@ -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';