diff --git a/src/components/admin/ErrorDetailsModal.tsx b/src/components/admin/ErrorDetailsModal.tsx index e6cfb519..1185dcf1 100644 --- a/src/components/admin/ErrorDetailsModal.tsx +++ b/src/components/admin/ErrorDetailsModal.tsx @@ -10,8 +10,8 @@ interface Breadcrumb { timestamp: string; category: string; message: string; - level: string; - data?: Record; + level?: string; + sequence_order?: number; } interface ErrorDetails { @@ -25,7 +25,7 @@ interface ErrorDetails { status_code: number; duration_ms: number; user_id?: string; - breadcrumbs?: Breadcrumb[]; + request_breadcrumbs?: Breadcrumb[]; environment_context?: Record; } @@ -146,26 +146,26 @@ ${error.error_stack ? `Stack Trace:\n${error.error_stack}` : ''} - {error.breadcrumbs && error.breadcrumbs.length > 0 ? ( + {error.request_breadcrumbs && error.request_breadcrumbs.length > 0 ? (
- {error.breadcrumbs.map((crumb, index) => ( -
-
- - {crumb.category} - - - {format(new Date(crumb.timestamp), 'HH:mm:ss.SSS')} - + {error.request_breadcrumbs + .sort((a, b) => (a.sequence_order || 0) - (b.sequence_order || 0)) + .map((crumb, index) => ( +
+
+ + {crumb.category} + + + {crumb.level || 'info'} + + + {format(new Date(crumb.timestamp), 'HH:mm:ss.SSS')} + +
+

{crumb.message}

-

{crumb.message}

- {crumb.data && ( -
-                        {JSON.stringify(crumb.data, null, 2)}
-                      
- )} -
- ))} + ))}
) : (

No breadcrumbs recorded

diff --git a/src/components/admin/ProfileAuditLog.tsx b/src/components/admin/ProfileAuditLog.tsx index 76a108ca..b6b2849c 100644 --- a/src/components/admin/ProfileAuditLog.tsx +++ b/src/components/admin/ProfileAuditLog.tsx @@ -8,8 +8,18 @@ import { format } from 'date-fns'; import { handleError } from '@/lib/errorHandler'; import { AuditLogEntry } from '@/types/database'; +interface ProfileChangeField { + field_name: string; + old_value: string | null; + new_value: string | null; +} + +interface ProfileAuditLogWithChanges extends Omit { + profile_change_fields?: ProfileChangeField[]; +} + export function ProfileAuditLog(): React.JSX.Element { - const [logs, setLogs] = useState([]); + const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { @@ -22,13 +32,18 @@ export function ProfileAuditLog(): React.JSX.Element { .from('profile_audit_log') .select(` *, - profiles!user_id(username, display_name) + profiles!user_id(username, display_name), + profile_change_fields( + field_name, + old_value, + new_value + ) `) .order('created_at', { ascending: false }) .limit(50); if (error) throw error; - setLogs((data || []) as AuditLogEntry[]); + setLogs((data || []) as ProfileAuditLogWithChanges[]); } catch (error: unknown) { handleError(error, { action: 'Load audit logs' }); } finally { @@ -71,7 +86,20 @@ export function ProfileAuditLog(): React.JSX.Element { {log.action} -
{JSON.stringify(log.changes || {}, null, 2)}
+ {log.profile_change_fields && log.profile_change_fields.length > 0 ? ( +
+ {log.profile_change_fields.map((change, idx) => ( +
+ {change.field_name}:{' '} + {change.old_value || 'null'} + {' → '} + {change.new_value || 'null'} +
+ ))} +
+ ) : ( + No changes + )}
{format(new Date(log.created_at), 'PPpp')} diff --git a/src/components/settings/DataExportTab.tsx b/src/components/settings/DataExportTab.tsx index fd8303fa..46f71d81 100644 --- a/src/components/settings/DataExportTab.tsx +++ b/src/components/settings/DataExportTab.tsx @@ -99,7 +99,15 @@ export function DataExportTab() { try { const { data, error } = await supabase .from('profile_audit_log') - .select('id, action, changes, created_at, changed_by, ip_address_hash, user_agent') + .select(` + id, + action, + created_at, + changed_by, + ip_address_hash, + user_agent, + profile_change_fields(field_name, old_value, new_value) + `) .eq('user_id', user.id) .order('created_at', { ascending: false }) .limit(10); @@ -115,15 +123,27 @@ export function DataExportTab() { } // Transform the data to match our type - const activityData: ActivityLogEntry[] = (data || []).map(item => ({ - id: item.id, - action: item.action, - changes: item.changes as Record, - created_at: item.created_at, - changed_by: item.changed_by, - ip_address_hash: item.ip_address_hash || undefined, - user_agent: item.user_agent || undefined - })); + const activityData: ActivityLogEntry[] = (data || []).map(item => { + const changes: Record = {}; + if (item.profile_change_fields) { + for (const field of item.profile_change_fields) { + changes[field.field_name] = { + old: field.old_value, + new: field.new_value + }; + } + } + + return { + id: item.id, + action: item.action, + changes, + created_at: item.created_at, + changed_by: item.changed_by, + ip_address_hash: item.ip_address_hash || undefined, + user_agent: item.user_agent || undefined + }; + }); setRecentActivity(activityData); diff --git a/src/hooks/moderation/useRealtimeSubscriptions.ts b/src/hooks/moderation/useRealtimeSubscriptions.ts index 533491dd..936d0e29 100644 --- a/src/hooks/moderation/useRealtimeSubscriptions.ts +++ b/src/hooks/moderation/useRealtimeSubscriptions.ts @@ -154,13 +154,18 @@ export function useRealtimeSubscriptions( const { data: submission, error } = await supabase .from('content_submissions') .select(` - id, submission_type, status, content, created_at, user_id, + id, submission_type, status, created_at, user_id, reviewed_at, reviewer_id, reviewer_notes, escalated, assigned_to, locked_until, submission_items ( id, item_type, item_data, status + ), + submission_metadata ( + entity_id, + park_id, + ride_id ) `) .eq('id', submissionId) @@ -177,14 +182,18 @@ export function useRealtimeSubscriptions( /** * Resolve entity names for a submission */ - const resolveEntityNames = useCallback(async (submission: { submission_type: string; content: Json }) => { - const content = submission.content as SubmissionContent; - let entityName = content?.name || 'Unknown'; + const resolveEntityNames = useCallback(async (submission: { submission_type: string; submission_metadata?: any[] }) => { + // Get metadata + const metadata = Array.isArray(submission.submission_metadata) && submission.submission_metadata.length > 0 + ? submission.submission_metadata[0] + : undefined; + + let entityName = 'Unknown'; let parkName: string | undefined; - if (submission.submission_type === 'ride' && content?.entity_id) { + if (submission.submission_type === 'ride' && metadata?.entity_id) { // Try cache first - const cachedRide = entityCache.getCached('rides', content.entity_id); + const cachedRide = entityCache.getCached('rides', metadata.entity_id); if (cachedRide) { entityName = cachedRide.name; if (cachedRide.park_id) { @@ -195,12 +204,12 @@ export function useRealtimeSubscriptions( const { data: ride } = await supabase .from('rides') .select('id, name, park_id') - .eq('id', content.entity_id) + .eq('id', metadata.entity_id) .maybeSingle(); if (ride) { entityName = ride.name; - entityCache.setCached('rides', content.entity_id, ride); + entityCache.setCached('rides', metadata.entity_id, ride); if (ride.park_id) { const { data: park } = await supabase @@ -216,36 +225,36 @@ export function useRealtimeSubscriptions( } } } - } else if (submission.submission_type === 'park' && content?.entity_id) { - const cachedPark = entityCache.getCached('parks', content.entity_id); + } else if (submission.submission_type === 'park' && metadata?.entity_id) { + const cachedPark = entityCache.getCached('parks', metadata.entity_id); if (cachedPark) { entityName = cachedPark.name; } else { const { data: park } = await supabase .from('parks') .select('id, name') - .eq('id', content.entity_id) + .eq('id', metadata.entity_id) .maybeSingle(); if (park) { entityName = park.name; - entityCache.setCached('parks', content.entity_id, park); + entityCache.setCached('parks', metadata.entity_id, park); } } - } else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type) && content?.entity_id) { - const cachedCompany = entityCache.getCached('companies', content.entity_id); + } else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type) && metadata?.entity_id) { + const cachedCompany = entityCache.getCached('companies', metadata.entity_id); if (cachedCompany) { entityName = cachedCompany.name; } else { const { data: company } = await supabase .from('companies') .select('id, name') - .eq('id', content.entity_id) + .eq('id', metadata.entity_id) .maybeSingle(); if (company) { entityName = company.name; - entityCache.setCached('companies', content.entity_id, company); + entityCache.setCached('companies', metadata.entity_id, company); } } } diff --git a/src/lib/systemActivityService.ts b/src/lib/systemActivityService.ts index 0a3c57f7..faf9b248 100644 --- a/src/lib/systemActivityService.ts +++ b/src/lib/systemActivityService.ts @@ -322,14 +322,30 @@ export async function fetchSystemActivities( } // Fetch admin audit log (admin actions) + // Note: Details are now in admin_audit_details table const { data: auditLogs, error: auditError } = await supabase .from('admin_audit_log') - .select('id, admin_user_id, target_user_id, action, details, created_at') + .select(` + id, + admin_user_id, + target_user_id, + action, + created_at, + admin_audit_details(detail_key, detail_value) + `) .order('created_at', { ascending: false }) .limit(limit); if (!auditError && auditLogs) { for (const log of auditLogs) { + // Convert relational details back to object format + const details: Record = {}; + if (log.admin_audit_details) { + for (const detail of log.admin_audit_details as any[]) { + details[detail.detail_key] = detail.detail_value; + } + } + activities.push({ id: log.id, type: 'admin_action', @@ -339,16 +355,24 @@ export async function fetchSystemActivities( details: { action: log.action, target_user_id: log.target_user_id, - details: log.details, + details, } as AdminActionDetails, }); } } // Fetch submission reviews (approved/rejected submissions) + // Note: Content is now in submission_metadata table, but entity_name is cached in view const { data: submissions, error: submissionsError } = await supabase .from('content_submissions') - .select('id, submission_type, status, reviewer_id, reviewed_at, content') + .select(` + id, + submission_type, + status, + reviewer_id, + reviewed_at, + submission_metadata(name) + `) .not('reviewed_at', 'is', null) .in('status', ['approved', 'rejected', 'partially_approved']) .order('reviewed_at', { ascending: false }) @@ -385,7 +409,12 @@ export async function fetchSystemActivities( ); for (const submission of submissions) { - const contentData = submission.content as SubmissionContent; + // Get name from submission_metadata + const metadata = submission.submission_metadata as any; + const entityName = Array.isArray(metadata) && metadata.length > 0 + ? metadata[0]?.name + : undefined; + const submissionItem = itemsMap.get(submission.id); const itemData = submissionItem?.item_data as any; @@ -394,7 +423,7 @@ export async function fetchSystemActivities( submission_id: submission.id, submission_type: submission.submission_type, status: submission.status, - entity_name: contentData?.name, + entity_name: entityName, }; // Enrich with photo-specific data for photo submissions @@ -600,7 +629,14 @@ export async function fetchSystemActivities( // 3. User bans/unbans from admin audit log const { data: banActions, error: banError } = await supabase .from('admin_audit_log') - .select('id, admin_user_id, target_user_id, action, details, created_at') + .select(` + id, + admin_user_id, + target_user_id, + action, + created_at, + admin_audit_details(detail_key, detail_value) + `) .in('action', ['user_banned', 'user_unbanned']) .order('created_at', { ascending: false }) .limit(limit); @@ -608,6 +644,15 @@ export async function fetchSystemActivities( if (!banError && banActions) { for (const action of banActions) { const activityType = action.action === 'user_banned' ? 'user_banned' : 'user_unbanned'; + + // Convert relational details back to object format + const details: Record = {}; + if (action.admin_audit_details) { + for (const detail of action.admin_audit_details as any[]) { + details[detail.detail_key] = detail.detail_value; + } + } + activities.push({ id: action.id, type: activityType, @@ -617,7 +662,7 @@ export async function fetchSystemActivities( details: { user_id: action.target_user_id, action: action.action === 'user_banned' ? 'banned' : 'unbanned', - reason: typeof action.details === 'object' && action.details && 'reason' in action.details ? String(action.details.reason) : undefined, + reason: details.reason, } as AccountLifecycleDetails, }); } diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 97dccbdd..6365d426 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -287,7 +287,13 @@ export default function Profile() { // Fetch last 10 submissions with enriched data let submissionsQuery = supabase .from('content_submissions') - .select('id, submission_type, content, status, created_at') + .select(` + id, + submission_type, + status, + created_at, + submission_metadata(name) + `) .eq('user_id', userId) .order('created_at', { ascending: false }) .limit(10); @@ -305,6 +311,12 @@ export default function Profile() { const enrichedSubmissions = await Promise.all((submissions || []).map(async (sub) => { const enriched: any = { ...sub }; + // Get name from submission_metadata + const metadata = sub.submission_metadata as any; + enriched.name = Array.isArray(metadata) && metadata.length > 0 + ? metadata[0]?.name + : undefined; + // For photo submissions, get photo count and preview if (sub.submission_type === 'photo') { const { data: photoSubs } = await supabase diff --git a/src/pages/admin/ErrorLookup.tsx b/src/pages/admin/ErrorLookup.tsx index afa76aec..fd529636 100644 --- a/src/pages/admin/ErrorLookup.tsx +++ b/src/pages/admin/ErrorLookup.tsx @@ -24,7 +24,16 @@ export default function ErrorLookup() { // Search by partial or full request ID const { data, error } = await supabase .from('request_metadata') - .select('*') + .select(` + *, + request_breadcrumbs( + timestamp, + category, + message, + level, + sequence_order + ) + `) .ilike('request_id', `${errorId}%`) .not('error_type', 'is', null) .limit(1) diff --git a/src/pages/admin/ErrorMonitoring.tsx b/src/pages/admin/ErrorMonitoring.tsx index d97afc1e..fe8a16f7 100644 --- a/src/pages/admin/ErrorMonitoring.tsx +++ b/src/pages/admin/ErrorMonitoring.tsx @@ -31,7 +31,16 @@ export default function ErrorMonitoring() { let query = supabase .from('request_metadata') - .select('*') + .select(` + *, + request_breadcrumbs( + timestamp, + category, + message, + level, + sequence_order + ) + `) .not('error_type', 'is', null) .gte('created_at', `now() - interval '${dateMap[dateRange]}'`) .order('created_at', { ascending: false })