mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 05:11:14 -05:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user