mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 11: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:
202
src/components/admin/ApprovalFailureModal.tsx
Normal file
202
src/components/admin/ApprovalFailureModal.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { format } from 'date-fns';
|
||||
import { XCircle, Clock, User, FileText, AlertTriangle } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface ApprovalFailure {
|
||||
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;
|
||||
success: boolean;
|
||||
moderator?: {
|
||||
username: string;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
submission?: {
|
||||
submission_type: string;
|
||||
user_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalFailureModalProps {
|
||||
failure: ApprovalFailure | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ApprovalFailureModal({ failure, onClose }: ApprovalFailureModalProps) {
|
||||
if (!failure) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={!!failure} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<XCircle className="w-5 h-5 text-destructive" />
|
||||
Approval Failure Details
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="error">Error Details</TabsTrigger>
|
||||
<TabsTrigger value="metadata">Metadata</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Timestamp</div>
|
||||
<div className="font-medium">
|
||||
{format(new Date(failure.created_at), 'PPpp')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Duration</div>
|
||||
<div className="font-medium flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
{failure.duration_ms != null ? `${failure.duration_ms}ms` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Submission Type</div>
|
||||
<Badge variant="outline">
|
||||
{failure.submission?.submission_type || 'Unknown'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Items Count</div>
|
||||
<div className="font-medium">{failure.items_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Moderator</div>
|
||||
<div className="font-medium flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
{failure.moderator?.username || 'Unknown'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Submission ID</div>
|
||||
<Link
|
||||
to={`/admin/moderation?submission=${failure.submission_id}`}
|
||||
className="font-mono text-sm text-primary hover:underline flex items-center gap-2"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
{failure.submission_id}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{failure.rollback_triggered && (
|
||||
<div className="flex items-center gap-2 p-3 bg-warning/10 text-warning rounded-md">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">
|
||||
Rollback was triggered for this approval
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="error" className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-2">Error Message</div>
|
||||
<div className="p-4 bg-destructive/10 text-destructive rounded-md font-mono text-sm">
|
||||
{failure.error_message || 'No error message available'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{failure.request_id && (
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-2">Request ID</div>
|
||||
<div className="p-3 bg-muted rounded-md font-mono text-sm">
|
||||
{failure.request_id}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 p-4 bg-muted rounded-md">
|
||||
<div className="text-sm font-medium mb-2">Troubleshooting Tips</div>
|
||||
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
|
||||
<li>Check if the submission still exists in the database</li>
|
||||
<li>Verify that all foreign key references are valid</li>
|
||||
<li>Review the edge function logs for detailed stack traces</li>
|
||||
<li>Check for concurrent modification conflicts</li>
|
||||
<li>Verify network connectivity and database availability</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="metadata" className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Failure ID</div>
|
||||
<div className="font-mono text-sm">{failure.id}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Success Status</div>
|
||||
<Badge variant="destructive">
|
||||
{failure.success ? 'Success' : 'Failed'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Moderator ID</div>
|
||||
<div className="font-mono text-sm">{failure.moderator_id}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Submitter ID</div>
|
||||
<div className="font-mono text-sm">{failure.submitter_id}</div>
|
||||
</div>
|
||||
|
||||
{failure.request_id && (
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Request ID</div>
|
||||
<div className="font-mono text-sm break-all">{failure.request_id}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Rollback Triggered</div>
|
||||
<Badge variant={failure.rollback_triggered ? 'destructive' : 'secondary'}>
|
||||
{failure.rollback_triggered ? 'Yes' : 'No'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { AlertCircle, TrendingUp, Users, Zap } from 'lucide-react';
|
||||
import { AlertCircle, TrendingUp, Users, Zap, CheckCircle, XCircle } from 'lucide-react';
|
||||
|
||||
interface ErrorSummary {
|
||||
error_type: string | null;
|
||||
@@ -9,82 +9,169 @@ interface ErrorSummary {
|
||||
avg_duration_ms: number | null;
|
||||
}
|
||||
|
||||
interface ErrorAnalyticsProps {
|
||||
errorSummary: ErrorSummary[] | undefined;
|
||||
interface ApprovalMetric {
|
||||
id: string;
|
||||
success: boolean;
|
||||
duration_ms: number | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export function ErrorAnalytics({ errorSummary }: ErrorAnalyticsProps) {
|
||||
if (!errorSummary || errorSummary.length === 0) {
|
||||
return null;
|
||||
interface ErrorAnalyticsProps {
|
||||
errorSummary: ErrorSummary[] | undefined;
|
||||
approvalMetrics: ApprovalMetric[] | undefined;
|
||||
}
|
||||
|
||||
export function ErrorAnalytics({ errorSummary, approvalMetrics }: ErrorAnalyticsProps) {
|
||||
// Calculate error metrics
|
||||
const totalErrors = errorSummary?.reduce((sum, item) => sum + (item.occurrence_count || 0), 0) || 0;
|
||||
const totalAffectedUsers = errorSummary?.reduce((sum, item) => sum + (item.affected_users || 0), 0) || 0;
|
||||
const avgErrorDuration = errorSummary?.length
|
||||
? errorSummary.reduce((sum, item) => sum + (item.avg_duration_ms || 0), 0) / errorSummary.length
|
||||
: 0;
|
||||
const topErrors = errorSummary?.slice(0, 5) || [];
|
||||
|
||||
// Calculate approval metrics
|
||||
const totalApprovals = approvalMetrics?.length || 0;
|
||||
const failedApprovals = approvalMetrics?.filter(m => !m.success).length || 0;
|
||||
const successRate = totalApprovals > 0 ? ((totalApprovals - failedApprovals) / totalApprovals) * 100 : 0;
|
||||
const avgApprovalDuration = approvalMetrics?.length
|
||||
? approvalMetrics.reduce((sum, m) => sum + (m.duration_ms || 0), 0) / approvalMetrics.length
|
||||
: 0;
|
||||
|
||||
// Show message if no data available
|
||||
if ((!errorSummary || errorSummary.length === 0) && (!approvalMetrics || approvalMetrics.length === 0)) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-center text-muted-foreground">No analytics data available</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const totalErrors = errorSummary.reduce((sum, item) => sum + (item.occurrence_count || 0), 0);
|
||||
const totalAffectedUsers = errorSummary.reduce((sum, item) => sum + (item.affected_users || 0), 0);
|
||||
const avgDuration = errorSummary.reduce((sum, item) => sum + (item.avg_duration_ms || 0), 0) / errorSummary.length;
|
||||
|
||||
const topErrors = errorSummary.slice(0, 5);
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Errors</CardTitle>
|
||||
<AlertCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalErrors}</div>
|
||||
<p className="text-xs text-muted-foreground">Last 30 days</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-6">
|
||||
{/* Error Metrics */}
|
||||
{errorSummary && errorSummary.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-3">Error Metrics</h3>
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Errors</CardTitle>
|
||||
<AlertCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalErrors}</div>
|
||||
<p className="text-xs text-muted-foreground">Last 30 days</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Error Types</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{errorSummary.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Unique error types</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Error Types</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{errorSummary.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Unique error types</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Affected Users</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalAffectedUsers}</div>
|
||||
<p className="text-xs text-muted-foreground">Users impacted</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Affected Users</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalAffectedUsers}</div>
|
||||
<p className="text-xs text-muted-foreground">Users impacted</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Avg Duration</CardTitle>
|
||||
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{Math.round(avgDuration)}ms</div>
|
||||
<p className="text-xs text-muted-foreground">Before error occurs</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Avg Duration</CardTitle>
|
||||
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{Math.round(avgErrorDuration)}ms</div>
|
||||
<p className="text-xs text-muted-foreground">Before error occurs</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Top 5 Errors</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={topErrors}>
|
||||
<XAxis dataKey="error_type" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="occurrence_count" fill="hsl(var(--destructive))" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top 5 Errors</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={topErrors}>
|
||||
<XAxis dataKey="error_type" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="occurrence_count" fill="hsl(var(--destructive))" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Approval Metrics */}
|
||||
{approvalMetrics && approvalMetrics.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-3">Approval Metrics</h3>
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Approvals</CardTitle>
|
||||
<CheckCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalApprovals}</div>
|
||||
<p className="text-xs text-muted-foreground">Last 24 hours</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Failures</CardTitle>
|
||||
<XCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-destructive">{failedApprovals}</div>
|
||||
<p className="text-xs text-muted-foreground">Failed approvals</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Success Rate</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{successRate.toFixed(1)}%</div>
|
||||
<p className="text-xs text-muted-foreground">Overall success rate</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Avg Duration</CardTitle>
|
||||
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{Math.round(avgApprovalDuration)}ms</div>
|
||||
<p className="text-xs text-muted-foreground">Approval time</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Admin components barrel exports
|
||||
export { AdminPageLayout } from './AdminPageLayout';
|
||||
export { ApprovalFailureModal } from './ApprovalFailureModal';
|
||||
export { BanUserDialog } from './BanUserDialog';
|
||||
export { DesignerForm } from './DesignerForm';
|
||||
export { HeadquartersLocationInput } from './HeadquartersLocationInput';
|
||||
|
||||
Reference in New Issue
Block a user