Implement Phase 3 fixes

The AI has implemented the Phase 3 plan, which includes adding approval failure monitoring to the existing error monitoring page, extending the ErrorAnalytics component with approval metrics, adding performance indexes, and creating the ApprovalFailureModal component.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-07 00:22:38 +00:00
parent 13c6e20f11
commit eac9902bb0
6 changed files with 1016 additions and 146 deletions

View File

@@ -6,9 +6,11 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { AlertCircle } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AlertCircle, XCircle } from 'lucide-react';
import { RefreshButton } from '@/components/ui/refresh-button';
import { ErrorDetailsModal } from '@/components/admin/ErrorDetailsModal';
import { ApprovalFailureModal } from '@/components/admin/ApprovalFailureModal';
import { ErrorAnalytics } from '@/components/admin/ErrorAnalytics';
import { format } from 'date-fns';
@@ -26,8 +28,33 @@ const getDateThreshold = (range: '1h' | '24h' | '7d' | '30d'): string => {
return threshold.toISOString();
};
interface EnrichedApprovalFailure {
id: string;
submission_id: string;
moderator_id: string;
submitter_id: string;
items_count: number;
duration_ms: number | null;
error_message: string | null;
request_id: string | null;
rollback_triggered: boolean | null;
created_at: string | null;
success: boolean;
moderator?: {
user_id: string;
username: string | null;
avatar_url: string | null;
};
submission?: {
id: string;
submission_type: string;
user_id: string;
};
}
export default function ErrorMonitoring() {
const [selectedError, setSelectedError] = useState<any>(null);
const [selectedFailure, setSelectedFailure] = useState<any>(null);
const [searchTerm, setSearchTerm] = useState('');
const [errorTypeFilter, setErrorTypeFilter] = useState<string>('all');
const [dateRange, setDateRange] = useState<'1h' | '24h' | '7d' | '30d'>('24h');
@@ -80,6 +107,63 @@ export default function ErrorMonitoring() {
},
});
// Fetch approval metrics (last 24h)
const { data: approvalMetrics } = useQuery({
queryKey: ['approval-metrics'],
queryFn: async () => {
const { data, error } = await supabase
.from('approval_transaction_metrics')
.select('id, success, duration_ms, created_at')
.gte('created_at', getDateThreshold('24h'))
.order('created_at', { ascending: false })
.limit(1000);
if (error) throw error;
return data;
},
});
// Fetch approval failures
const { data: approvalFailures, refetch: refetchFailures, isFetching: isFetchingFailures } = useQuery<EnrichedApprovalFailure[]>({
queryKey: ['approval-failures', dateRange, searchTerm],
queryFn: async () => {
let query = supabase
.from('approval_transaction_metrics')
.select('*')
.eq('success', false)
.gte('created_at', getDateThreshold(dateRange))
.order('created_at', { ascending: false })
.limit(50);
if (searchTerm) {
query = query.or(`submission_id.ilike.%${searchTerm}%,error_message.ilike.%${searchTerm}%`);
}
const { data, error } = await query;
if (error) throw error;
// Fetch moderator and submission data separately
if (data && data.length > 0) {
const moderatorIds = [...new Set(data.map(f => f.moderator_id))];
const submissionIds = [...new Set(data.map(f => f.submission_id))];
const [moderatorsData, submissionsData] = await Promise.all([
supabase.from('profiles').select('user_id, username, avatar_url').in('user_id', moderatorIds),
supabase.from('content_submissions').select('id, submission_type, user_id').in('id', submissionIds)
]);
// Enrich data with moderator and submission info
return data.map(failure => ({
...failure,
moderator: moderatorsData.data?.find(m => m.user_id === failure.moderator_id),
submission: submissionsData.data?.find(s => s.id === failure.submission_id)
})) as EnrichedApprovalFailure[];
}
return (data || []) as EnrichedApprovalFailure[];
},
refetchInterval: 30000,
});
return (
<AdminLayout>
<div className="space-y-6">
@@ -97,88 +181,172 @@ export default function ErrorMonitoring() {
</div>
{/* Analytics Section */}
<ErrorAnalytics errorSummary={errorSummary} />
<ErrorAnalytics errorSummary={errorSummary} approvalMetrics={approvalMetrics} />
{/* Filters */}
<Card>
<CardHeader>
<CardTitle>Error Log</CardTitle>
<CardDescription>Recent errors across the application</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-4 mb-6">
<div className="flex-1">
<Input
placeholder="Search by request ID, endpoint, or error message..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
<Select value={dateRange} onValueChange={(v: any) => setDateRange(v)}>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1h">Last Hour</SelectItem>
<SelectItem value="24h">Last 24 Hours</SelectItem>
<SelectItem value="7d">Last 7 Days</SelectItem>
<SelectItem value="30d">Last 30 Days</SelectItem>
</SelectContent>
</Select>
<Select value={errorTypeFilter} onValueChange={setErrorTypeFilter}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Error type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="FunctionsFetchError">Functions Fetch</SelectItem>
<SelectItem value="FunctionsHttpError">Functions HTTP</SelectItem>
<SelectItem value="Error">Generic Error</SelectItem>
</SelectContent>
</Select>
</div>
{/* Tabs for Errors and Approval Failures */}
<Tabs defaultValue="errors" className="w-full">
<TabsList>
<TabsTrigger value="errors">Application Errors</TabsTrigger>
<TabsTrigger value="approvals">Approval Failures</TabsTrigger>
</TabsList>
{/* Error List */}
{isLoading ? (
<div className="text-center py-8 text-muted-foreground">Loading errors...</div>
) : errors && errors.length > 0 ? (
<div className="space-y-2">
{errors.map((error) => (
<div
key={error.id}
onClick={() => setSelectedError(error)}
className="p-4 border rounded-lg hover:bg-accent cursor-pointer transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<AlertCircle className="w-4 h-4 text-destructive" />
<span className="font-medium">{error.error_type}</span>
<Badge variant="outline" className="text-xs">
{error.endpoint}
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-2">
{error.error_message}
</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>ID: {error.request_id.slice(0, 8)}</span>
<span>{format(new Date(error.created_at), 'PPp')}</span>
{error.duration_ms != null && <span>{error.duration_ms}ms</span>}
<TabsContent value="errors" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Error Log</CardTitle>
<CardDescription>Recent errors across the application</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-4 mb-6">
<div className="flex-1">
<Input
placeholder="Search by request ID, endpoint, or error message..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
<Select value={dateRange} onValueChange={(v: any) => setDateRange(v)}>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1h">Last Hour</SelectItem>
<SelectItem value="24h">Last 24 Hours</SelectItem>
<SelectItem value="7d">Last 7 Days</SelectItem>
<SelectItem value="30d">Last 30 Days</SelectItem>
</SelectContent>
</Select>
<Select value={errorTypeFilter} onValueChange={setErrorTypeFilter}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Error type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="FunctionsFetchError">Functions Fetch</SelectItem>
<SelectItem value="FunctionsHttpError">Functions HTTP</SelectItem>
<SelectItem value="Error">Generic Error</SelectItem>
</SelectContent>
</Select>
</div>
{isLoading ? (
<div className="text-center py-8 text-muted-foreground">Loading errors...</div>
) : errors && errors.length > 0 ? (
<div className="space-y-2">
{errors.map((error) => (
<div
key={error.id}
onClick={() => setSelectedError(error)}
className="p-4 border rounded-lg hover:bg-accent cursor-pointer transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<AlertCircle className="w-4 h-4 text-destructive" />
<span className="font-medium">{error.error_type}</span>
<Badge variant="outline" className="text-xs">
{error.endpoint}
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-2">
{error.error_message}
</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>ID: {error.request_id.slice(0, 8)}</span>
<span>{format(new Date(error.created_at), 'PPp')}</span>
{error.duration_ms != null && <span>{error.duration_ms}ms</span>}
</div>
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
No errors found for the selected filters
</div>
)}
</CardContent>
</Card>
) : (
<div className="text-center py-8 text-muted-foreground">
No errors found for the selected filters
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="approvals" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Approval Failures</CardTitle>
<CardDescription>Failed approval transactions requiring investigation</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-4 mb-6">
<div className="flex-1">
<Input
placeholder="Search by submission ID or error message..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
<Select value={dateRange} onValueChange={(v: any) => setDateRange(v)}>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1h">Last Hour</SelectItem>
<SelectItem value="24h">Last 24 Hours</SelectItem>
<SelectItem value="7d">Last 7 Days</SelectItem>
<SelectItem value="30d">Last 30 Days</SelectItem>
</SelectContent>
</Select>
</div>
{isFetchingFailures ? (
<div className="text-center py-8 text-muted-foreground">Loading approval failures...</div>
) : approvalFailures && approvalFailures.length > 0 ? (
<div className="space-y-2">
{approvalFailures.map((failure) => (
<div
key={failure.id}
onClick={() => setSelectedFailure(failure)}
className="p-4 border rounded-lg hover:bg-accent cursor-pointer transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<XCircle className="w-4 h-4 text-destructive" />
<span className="font-medium">Approval Failed</span>
<Badge variant="outline" className="text-xs">
{failure.submission?.submission_type || 'Unknown'}
</Badge>
{failure.rollback_triggered && (
<Badge variant="destructive" className="text-xs">
Rollback
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground mb-2">
{failure.error_message || 'No error message available'}
</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>Moderator: {failure.moderator?.username || 'Unknown'}</span>
<span>{failure.created_at && format(new Date(failure.created_at), 'PPp')}</span>
{failure.duration_ms != null && <span>{failure.duration_ms}ms</span>}
<span>{failure.items_count} items</span>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
No approval failures found for the selected filters
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
{/* Error Details Modal */}
@@ -188,6 +356,14 @@ export default function ErrorMonitoring() {
onClose={() => setSelectedError(null)}
/>
)}
{/* Approval Failure Modal */}
{selectedFailure && (
<ApprovalFailureModal
failure={selectedFailure}
onClose={() => setSelectedFailure(null)}
/>
)}
</AdminLayout>
);
}