mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-28 14:47:00 -05:00
Compare commits
2 Commits
19b1451f32
...
63d9d8890c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63d9d8890c | ||
|
|
a4e1be8056 |
@@ -10,8 +10,8 @@ interface Breadcrumb {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
category: string;
|
category: string;
|
||||||
message: string;
|
message: string;
|
||||||
level: string;
|
level?: string;
|
||||||
data?: Record<string, unknown>;
|
sequence_order?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorDetails {
|
interface ErrorDetails {
|
||||||
@@ -25,7 +25,7 @@ interface ErrorDetails {
|
|||||||
status_code: number;
|
status_code: number;
|
||||||
duration_ms: number;
|
duration_ms: number;
|
||||||
user_id?: string;
|
user_id?: string;
|
||||||
breadcrumbs?: Breadcrumb[];
|
request_breadcrumbs?: Breadcrumb[];
|
||||||
environment_context?: Record<string, unknown>;
|
environment_context?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,24 +146,24 @@ ${error.error_stack ? `Stack Trace:\n${error.error_stack}` : ''}
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="breadcrumbs">
|
<TabsContent value="breadcrumbs">
|
||||||
{error.breadcrumbs && error.breadcrumbs.length > 0 ? (
|
{error.request_breadcrumbs && error.request_breadcrumbs.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{error.breadcrumbs.map((crumb, index) => (
|
{error.request_breadcrumbs
|
||||||
|
.sort((a, b) => (a.sequence_order || 0) - (b.sequence_order || 0))
|
||||||
|
.map((crumb, index) => (
|
||||||
<div key={index} className="border-l-2 border-primary pl-4 py-2">
|
<div key={index} className="border-l-2 border-primary pl-4 py-2">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{crumb.category}
|
{crumb.category}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<Badge variant={crumb.level === 'error' ? 'destructive' : 'secondary'} className="text-xs">
|
||||||
|
{crumb.level || 'info'}
|
||||||
|
</Badge>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{format(new Date(crumb.timestamp), 'HH:mm:ss.SSS')}
|
{format(new Date(crumb.timestamp), 'HH:mm:ss.SSS')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm">{crumb.message}</p>
|
<p className="text-sm">{crumb.message}</p>
|
||||||
{crumb.data && (
|
|
||||||
<pre className="text-xs text-muted-foreground mt-1">
|
|
||||||
{JSON.stringify(crumb.data, null, 2)}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,8 +8,18 @@ import { format } from 'date-fns';
|
|||||||
import { handleError } from '@/lib/errorHandler';
|
import { handleError } from '@/lib/errorHandler';
|
||||||
import { AuditLogEntry } from '@/types/database';
|
import { AuditLogEntry } from '@/types/database';
|
||||||
|
|
||||||
|
interface ProfileChangeField {
|
||||||
|
field_name: string;
|
||||||
|
old_value: string | null;
|
||||||
|
new_value: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileAuditLogWithChanges extends Omit<AuditLogEntry, 'changes'> {
|
||||||
|
profile_change_fields?: ProfileChangeField[];
|
||||||
|
}
|
||||||
|
|
||||||
export function ProfileAuditLog(): React.JSX.Element {
|
export function ProfileAuditLog(): React.JSX.Element {
|
||||||
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
|
const [logs, setLogs] = useState<ProfileAuditLogWithChanges[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -22,13 +32,18 @@ export function ProfileAuditLog(): React.JSX.Element {
|
|||||||
.from('profile_audit_log')
|
.from('profile_audit_log')
|
||||||
.select(`
|
.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 })
|
.order('created_at', { ascending: false })
|
||||||
.limit(50);
|
.limit(50);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
setLogs((data || []) as AuditLogEntry[]);
|
setLogs((data || []) as ProfileAuditLogWithChanges[]);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
handleError(error, { action: 'Load audit logs' });
|
handleError(error, { action: 'Load audit logs' });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -71,7 +86,20 @@ export function ProfileAuditLog(): React.JSX.Element {
|
|||||||
<Badge variant="secondary">{log.action}</Badge>
|
<Badge variant="secondary">{log.action}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<pre className="text-xs">{JSON.stringify(log.changes || {}, null, 2)}</pre>
|
{log.profile_change_fields && log.profile_change_fields.length > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{log.profile_change_fields.map((change, idx) => (
|
||||||
|
<div key={idx} className="text-xs">
|
||||||
|
<span className="font-medium">{change.field_name}:</span>{' '}
|
||||||
|
<span className="text-muted-foreground">{change.old_value || 'null'}</span>
|
||||||
|
{' → '}
|
||||||
|
<span className="text-foreground">{change.new_value || 'null'}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">No changes</span>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-muted-foreground">
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
{format(new Date(log.created_at), 'PPpp')}
|
{format(new Date(log.created_at), 'PPpp')}
|
||||||
|
|||||||
@@ -99,7 +99,15 @@ export function DataExportTab() {
|
|||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('profile_audit_log')
|
.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)
|
.eq('user_id', user.id)
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.limit(10);
|
.limit(10);
|
||||||
@@ -115,15 +123,27 @@ export function DataExportTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transform the data to match our type
|
// Transform the data to match our type
|
||||||
const activityData: ActivityLogEntry[] = (data || []).map(item => ({
|
const activityData: ActivityLogEntry[] = (data || []).map(item => {
|
||||||
|
const changes: Record<string, any> = {};
|
||||||
|
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,
|
id: item.id,
|
||||||
action: item.action,
|
action: item.action,
|
||||||
changes: item.changes as Record<string, any>,
|
changes,
|
||||||
created_at: item.created_at,
|
created_at: item.created_at,
|
||||||
changed_by: item.changed_by,
|
changed_by: item.changed_by,
|
||||||
ip_address_hash: item.ip_address_hash || undefined,
|
ip_address_hash: item.ip_address_hash || undefined,
|
||||||
user_agent: item.user_agent || undefined
|
user_agent: item.user_agent || undefined
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
setRecentActivity(activityData);
|
setRecentActivity(activityData);
|
||||||
|
|
||||||
|
|||||||
@@ -154,13 +154,18 @@ export function useRealtimeSubscriptions(
|
|||||||
const { data: submission, error } = await supabase
|
const { data: submission, error } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.select(`
|
.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,
|
reviewed_at, reviewer_id, reviewer_notes, escalated, assigned_to, locked_until,
|
||||||
submission_items (
|
submission_items (
|
||||||
id,
|
id,
|
||||||
item_type,
|
item_type,
|
||||||
item_data,
|
item_data,
|
||||||
status
|
status
|
||||||
|
),
|
||||||
|
submission_metadata (
|
||||||
|
entity_id,
|
||||||
|
park_id,
|
||||||
|
ride_id
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
.eq('id', submissionId)
|
.eq('id', submissionId)
|
||||||
@@ -177,14 +182,18 @@ export function useRealtimeSubscriptions(
|
|||||||
/**
|
/**
|
||||||
* Resolve entity names for a submission
|
* Resolve entity names for a submission
|
||||||
*/
|
*/
|
||||||
const resolveEntityNames = useCallback(async (submission: { submission_type: string; content: Json }) => {
|
const resolveEntityNames = useCallback(async (submission: { submission_type: string; submission_metadata?: any[] }) => {
|
||||||
const content = submission.content as SubmissionContent;
|
// Get metadata
|
||||||
let entityName = content?.name || 'Unknown';
|
const metadata = Array.isArray(submission.submission_metadata) && submission.submission_metadata.length > 0
|
||||||
|
? submission.submission_metadata[0]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let entityName = 'Unknown';
|
||||||
let parkName: string | undefined;
|
let parkName: string | undefined;
|
||||||
|
|
||||||
if (submission.submission_type === 'ride' && content?.entity_id) {
|
if (submission.submission_type === 'ride' && metadata?.entity_id) {
|
||||||
// Try cache first
|
// Try cache first
|
||||||
const cachedRide = entityCache.getCached('rides', content.entity_id);
|
const cachedRide = entityCache.getCached('rides', metadata.entity_id);
|
||||||
if (cachedRide) {
|
if (cachedRide) {
|
||||||
entityName = cachedRide.name;
|
entityName = cachedRide.name;
|
||||||
if (cachedRide.park_id) {
|
if (cachedRide.park_id) {
|
||||||
@@ -195,12 +204,12 @@ export function useRealtimeSubscriptions(
|
|||||||
const { data: ride } = await supabase
|
const { data: ride } = await supabase
|
||||||
.from('rides')
|
.from('rides')
|
||||||
.select('id, name, park_id')
|
.select('id, name, park_id')
|
||||||
.eq('id', content.entity_id)
|
.eq('id', metadata.entity_id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (ride) {
|
if (ride) {
|
||||||
entityName = ride.name;
|
entityName = ride.name;
|
||||||
entityCache.setCached('rides', content.entity_id, ride);
|
entityCache.setCached('rides', metadata.entity_id, ride);
|
||||||
|
|
||||||
if (ride.park_id) {
|
if (ride.park_id) {
|
||||||
const { data: park } = await supabase
|
const { data: park } = await supabase
|
||||||
@@ -216,36 +225,36 @@ export function useRealtimeSubscriptions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (submission.submission_type === 'park' && content?.entity_id) {
|
} else if (submission.submission_type === 'park' && metadata?.entity_id) {
|
||||||
const cachedPark = entityCache.getCached('parks', content.entity_id);
|
const cachedPark = entityCache.getCached('parks', metadata.entity_id);
|
||||||
if (cachedPark) {
|
if (cachedPark) {
|
||||||
entityName = cachedPark.name;
|
entityName = cachedPark.name;
|
||||||
} else {
|
} else {
|
||||||
const { data: park } = await supabase
|
const { data: park } = await supabase
|
||||||
.from('parks')
|
.from('parks')
|
||||||
.select('id, name')
|
.select('id, name')
|
||||||
.eq('id', content.entity_id)
|
.eq('id', metadata.entity_id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (park) {
|
if (park) {
|
||||||
entityName = park.name;
|
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) {
|
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type) && metadata?.entity_id) {
|
||||||
const cachedCompany = entityCache.getCached('companies', content.entity_id);
|
const cachedCompany = entityCache.getCached('companies', metadata.entity_id);
|
||||||
if (cachedCompany) {
|
if (cachedCompany) {
|
||||||
entityName = cachedCompany.name;
|
entityName = cachedCompany.name;
|
||||||
} else {
|
} else {
|
||||||
const { data: company } = await supabase
|
const { data: company } = await supabase
|
||||||
.from('companies')
|
.from('companies')
|
||||||
.select('id, name')
|
.select('id, name')
|
||||||
.eq('id', content.entity_id)
|
.eq('id', metadata.entity_id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (company) {
|
if (company) {
|
||||||
entityName = company.name;
|
entityName = company.name;
|
||||||
entityCache.setCached('companies', content.entity_id, company);
|
entityCache.setCached('companies', metadata.entity_id, company);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ export type Database = {
|
|||||||
admin_user_id: string
|
admin_user_id: string
|
||||||
auth0_event_type: string | null
|
auth0_event_type: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
details: Json | null
|
|
||||||
id: string
|
id: string
|
||||||
target_user_id: string
|
target_user_id: string
|
||||||
}
|
}
|
||||||
@@ -106,7 +105,6 @@ export type Database = {
|
|||||||
admin_user_id: string
|
admin_user_id: string
|
||||||
auth0_event_type?: string | null
|
auth0_event_type?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
details?: Json | null
|
|
||||||
id?: string
|
id?: string
|
||||||
target_user_id: string
|
target_user_id: string
|
||||||
}
|
}
|
||||||
@@ -115,7 +113,6 @@ export type Database = {
|
|||||||
admin_user_id?: string
|
admin_user_id?: string
|
||||||
auth0_event_type?: string | null
|
auth0_event_type?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
details?: Json | null
|
|
||||||
id?: string
|
id?: string
|
||||||
target_user_id?: string
|
target_user_id?: string
|
||||||
}
|
}
|
||||||
@@ -536,7 +533,6 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
conflict_resolutions: {
|
conflict_resolutions: {
|
||||||
Row: {
|
Row: {
|
||||||
conflict_details: Json | null
|
|
||||||
created_at: string
|
created_at: string
|
||||||
detected_at: string
|
detected_at: string
|
||||||
id: string
|
id: string
|
||||||
@@ -545,7 +541,6 @@ export type Database = {
|
|||||||
submission_id: string
|
submission_id: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
conflict_details?: Json | null
|
|
||||||
created_at?: string
|
created_at?: string
|
||||||
detected_at?: string
|
detected_at?: string
|
||||||
id?: string
|
id?: string
|
||||||
@@ -554,7 +549,6 @@ export type Database = {
|
|||||||
submission_id: string
|
submission_id: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
conflict_details?: Json | null
|
|
||||||
created_at?: string
|
created_at?: string
|
||||||
detected_at?: string
|
detected_at?: string
|
||||||
id?: string
|
id?: string
|
||||||
@@ -592,7 +586,6 @@ export type Database = {
|
|||||||
in_reply_to: string | null
|
in_reply_to: string | null
|
||||||
is_auto_reply: boolean | null
|
is_auto_reply: boolean | null
|
||||||
message_id: string
|
message_id: string
|
||||||
metadata: Json | null
|
|
||||||
reference_chain: string[] | null
|
reference_chain: string[] | null
|
||||||
sent_by: string | null
|
sent_by: string | null
|
||||||
smtp_message_id: string | null
|
smtp_message_id: string | null
|
||||||
@@ -613,7 +606,6 @@ export type Database = {
|
|||||||
in_reply_to?: string | null
|
in_reply_to?: string | null
|
||||||
is_auto_reply?: boolean | null
|
is_auto_reply?: boolean | null
|
||||||
message_id: string
|
message_id: string
|
||||||
metadata?: Json | null
|
|
||||||
reference_chain?: string[] | null
|
reference_chain?: string[] | null
|
||||||
sent_by?: string | null
|
sent_by?: string | null
|
||||||
smtp_message_id?: string | null
|
smtp_message_id?: string | null
|
||||||
@@ -634,7 +626,6 @@ export type Database = {
|
|||||||
in_reply_to?: string | null
|
in_reply_to?: string | null
|
||||||
is_auto_reply?: boolean | null
|
is_auto_reply?: boolean | null
|
||||||
message_id?: string
|
message_id?: string
|
||||||
metadata?: Json | null
|
|
||||||
reference_chain?: string[] | null
|
reference_chain?: string[] | null
|
||||||
sent_by?: string | null
|
sent_by?: string | null
|
||||||
smtp_message_id?: string | null
|
smtp_message_id?: string | null
|
||||||
@@ -701,7 +692,6 @@ export type Database = {
|
|||||||
response_count: number | null
|
response_count: number | null
|
||||||
status: string
|
status: string
|
||||||
subject: string
|
subject: string
|
||||||
submitter_profile_data: Json | null
|
|
||||||
submitter_profile_id: string | null
|
submitter_profile_id: string | null
|
||||||
submitter_reputation: number | null
|
submitter_reputation: number | null
|
||||||
submitter_username: string | null
|
submitter_username: string | null
|
||||||
@@ -730,7 +720,6 @@ export type Database = {
|
|||||||
response_count?: number | null
|
response_count?: number | null
|
||||||
status?: string
|
status?: string
|
||||||
subject: string
|
subject: string
|
||||||
submitter_profile_data?: Json | null
|
|
||||||
submitter_profile_id?: string | null
|
submitter_profile_id?: string | null
|
||||||
submitter_reputation?: number | null
|
submitter_reputation?: number | null
|
||||||
submitter_username?: string | null
|
submitter_username?: string | null
|
||||||
@@ -759,7 +748,6 @@ export type Database = {
|
|||||||
response_count?: number | null
|
response_count?: number | null
|
||||||
status?: string
|
status?: string
|
||||||
subject?: string
|
subject?: string
|
||||||
submitter_profile_data?: Json | null
|
|
||||||
submitter_profile_id?: string | null
|
submitter_profile_id?: string | null
|
||||||
submitter_reputation?: number | null
|
submitter_reputation?: number | null
|
||||||
submitter_username?: string | null
|
submitter_username?: string | null
|
||||||
@@ -791,7 +779,6 @@ export type Database = {
|
|||||||
approval_mode: string | null
|
approval_mode: string | null
|
||||||
assigned_at: string | null
|
assigned_at: string | null
|
||||||
assigned_to: string | null
|
assigned_to: string | null
|
||||||
content: Json
|
|
||||||
created_at: string
|
created_at: string
|
||||||
escalated: boolean | null
|
escalated: boolean | null
|
||||||
escalated_at: string | null
|
escalated_at: string | null
|
||||||
@@ -819,7 +806,6 @@ export type Database = {
|
|||||||
approval_mode?: string | null
|
approval_mode?: string | null
|
||||||
assigned_at?: string | null
|
assigned_at?: string | null
|
||||||
assigned_to?: string | null
|
assigned_to?: string | null
|
||||||
content: Json
|
|
||||||
created_at?: string
|
created_at?: string
|
||||||
escalated?: boolean | null
|
escalated?: boolean | null
|
||||||
escalated_at?: string | null
|
escalated_at?: string | null
|
||||||
@@ -847,7 +833,6 @@ export type Database = {
|
|||||||
approval_mode?: string | null
|
approval_mode?: string | null
|
||||||
assigned_at?: string | null
|
assigned_at?: string | null
|
||||||
assigned_to?: string | null
|
assigned_to?: string | null
|
||||||
content?: Json
|
|
||||||
created_at?: string
|
created_at?: string
|
||||||
escalated?: boolean | null
|
escalated?: boolean | null
|
||||||
escalated_at?: string | null
|
escalated_at?: string | null
|
||||||
@@ -1489,21 +1474,18 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
item_edit_history: {
|
item_edit_history: {
|
||||||
Row: {
|
Row: {
|
||||||
changes: Json
|
|
||||||
edited_at: string
|
edited_at: string
|
||||||
editor_id: string
|
editor_id: string
|
||||||
id: string
|
id: string
|
||||||
item_id: string
|
item_id: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
changes: Json
|
|
||||||
edited_at?: string
|
edited_at?: string
|
||||||
editor_id: string
|
editor_id: string
|
||||||
id?: string
|
id?: string
|
||||||
item_id: string
|
item_id: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
changes?: Json
|
|
||||||
edited_at?: string
|
edited_at?: string
|
||||||
editor_id?: string
|
editor_id?: string
|
||||||
id?: string
|
id?: string
|
||||||
@@ -1605,7 +1587,6 @@ export type Database = {
|
|||||||
created_at: string
|
created_at: string
|
||||||
id: string
|
id: string
|
||||||
is_test_data: boolean | null
|
is_test_data: boolean | null
|
||||||
metadata: Json | null
|
|
||||||
moderator_id: string
|
moderator_id: string
|
||||||
new_status: string | null
|
new_status: string | null
|
||||||
notes: string | null
|
notes: string | null
|
||||||
@@ -1617,7 +1598,6 @@ export type Database = {
|
|||||||
created_at?: string
|
created_at?: string
|
||||||
id?: string
|
id?: string
|
||||||
is_test_data?: boolean | null
|
is_test_data?: boolean | null
|
||||||
metadata?: Json | null
|
|
||||||
moderator_id: string
|
moderator_id: string
|
||||||
new_status?: string | null
|
new_status?: string | null
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
@@ -1629,7 +1609,6 @@ export type Database = {
|
|||||||
created_at?: string
|
created_at?: string
|
||||||
id?: string
|
id?: string
|
||||||
is_test_data?: boolean | null
|
is_test_data?: boolean | null
|
||||||
metadata?: Json | null
|
|
||||||
moderator_id?: string
|
moderator_id?: string
|
||||||
new_status?: string | null
|
new_status?: string | null
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
@@ -1787,7 +1766,6 @@ export type Database = {
|
|||||||
idempotency_key: string | null
|
idempotency_key: string | null
|
||||||
is_duplicate: boolean
|
is_duplicate: boolean
|
||||||
novu_transaction_id: string | null
|
novu_transaction_id: string | null
|
||||||
payload: Json | null
|
|
||||||
read_at: string | null
|
read_at: string | null
|
||||||
status: string
|
status: string
|
||||||
template_id: string | null
|
template_id: string | null
|
||||||
@@ -1802,7 +1780,6 @@ export type Database = {
|
|||||||
idempotency_key?: string | null
|
idempotency_key?: string | null
|
||||||
is_duplicate?: boolean
|
is_duplicate?: boolean
|
||||||
novu_transaction_id?: string | null
|
novu_transaction_id?: string | null
|
||||||
payload?: Json | null
|
|
||||||
read_at?: string | null
|
read_at?: string | null
|
||||||
status?: string
|
status?: string
|
||||||
template_id?: string | null
|
template_id?: string | null
|
||||||
@@ -1817,7 +1794,6 @@ export type Database = {
|
|||||||
idempotency_key?: string | null
|
idempotency_key?: string | null
|
||||||
is_duplicate?: boolean
|
is_duplicate?: boolean
|
||||||
novu_transaction_id?: string | null
|
novu_transaction_id?: string | null
|
||||||
payload?: Json | null
|
|
||||||
read_at?: string | null
|
read_at?: string | null
|
||||||
status?: string
|
status?: string
|
||||||
template_id?: string | null
|
template_id?: string | null
|
||||||
@@ -2513,7 +2489,6 @@ export type Database = {
|
|||||||
Row: {
|
Row: {
|
||||||
action: string
|
action: string
|
||||||
changed_by: string
|
changed_by: string
|
||||||
changes: Json
|
|
||||||
created_at: string
|
created_at: string
|
||||||
id: string
|
id: string
|
||||||
ip_address_hash: string | null
|
ip_address_hash: string | null
|
||||||
@@ -2523,7 +2498,6 @@ export type Database = {
|
|||||||
Insert: {
|
Insert: {
|
||||||
action: string
|
action: string
|
||||||
changed_by: string
|
changed_by: string
|
||||||
changes: Json
|
|
||||||
created_at?: string
|
created_at?: string
|
||||||
id?: string
|
id?: string
|
||||||
ip_address_hash?: string | null
|
ip_address_hash?: string | null
|
||||||
@@ -2533,7 +2507,6 @@ export type Database = {
|
|||||||
Update: {
|
Update: {
|
||||||
action?: string
|
action?: string
|
||||||
changed_by?: string
|
changed_by?: string
|
||||||
changes?: Json
|
|
||||||
created_at?: string
|
created_at?: string
|
||||||
id?: string
|
id?: string
|
||||||
ip_address_hash?: string | null
|
ip_address_hash?: string | null
|
||||||
@@ -2809,13 +2782,11 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
request_metadata: {
|
request_metadata: {
|
||||||
Row: {
|
Row: {
|
||||||
breadcrumbs: Json | null
|
|
||||||
client_version: string | null
|
client_version: string | null
|
||||||
completed_at: string | null
|
completed_at: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
duration_ms: number | null
|
duration_ms: number | null
|
||||||
endpoint: string
|
endpoint: string
|
||||||
environment_context: Json | null
|
|
||||||
error_message: string | null
|
error_message: string | null
|
||||||
error_stack: string | null
|
error_stack: string | null
|
||||||
error_type: string | null
|
error_type: string | null
|
||||||
@@ -2837,13 +2808,11 @@ export type Database = {
|
|||||||
user_id: string | null
|
user_id: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
breadcrumbs?: Json | null
|
|
||||||
client_version?: string | null
|
client_version?: string | null
|
||||||
completed_at?: string | null
|
completed_at?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
duration_ms?: number | null
|
duration_ms?: number | null
|
||||||
endpoint: string
|
endpoint: string
|
||||||
environment_context?: Json | null
|
|
||||||
error_message?: string | null
|
error_message?: string | null
|
||||||
error_stack?: string | null
|
error_stack?: string | null
|
||||||
error_type?: string | null
|
error_type?: string | null
|
||||||
@@ -2865,13 +2834,11 @@ export type Database = {
|
|||||||
user_id?: string | null
|
user_id?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
breadcrumbs?: Json | null
|
|
||||||
client_version?: string | null
|
client_version?: string | null
|
||||||
completed_at?: string | null
|
completed_at?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
duration_ms?: number | null
|
duration_ms?: number | null
|
||||||
endpoint?: string
|
endpoint?: string
|
||||||
environment_context?: Json | null
|
|
||||||
error_message?: string | null
|
error_message?: string | null
|
||||||
error_stack?: string | null
|
error_stack?: string | null
|
||||||
error_type?: string | null
|
error_type?: string | null
|
||||||
@@ -3003,7 +2970,6 @@ export type Database = {
|
|||||||
moderated_by: string | null
|
moderated_by: string | null
|
||||||
moderation_status: string
|
moderation_status: string
|
||||||
park_id: string | null
|
park_id: string | null
|
||||||
photos: Json | null
|
|
||||||
rating: number
|
rating: number
|
||||||
report_count: number
|
report_count: number
|
||||||
ride_id: string | null
|
ride_id: string | null
|
||||||
@@ -3024,7 +2990,6 @@ export type Database = {
|
|||||||
moderated_by?: string | null
|
moderated_by?: string | null
|
||||||
moderation_status?: string
|
moderation_status?: string
|
||||||
park_id?: string | null
|
park_id?: string | null
|
||||||
photos?: Json | null
|
|
||||||
rating: number
|
rating: number
|
||||||
report_count?: number
|
report_count?: number
|
||||||
ride_id?: string | null
|
ride_id?: string | null
|
||||||
@@ -3045,7 +3010,6 @@ export type Database = {
|
|||||||
moderated_by?: string | null
|
moderated_by?: string | null
|
||||||
moderation_status?: string
|
moderation_status?: string
|
||||||
park_id?: string | null
|
park_id?: string | null
|
||||||
photos?: Json | null
|
|
||||||
rating?: number
|
rating?: number
|
||||||
report_count?: number
|
report_count?: number
|
||||||
ride_id?: string | null
|
ride_id?: string | null
|
||||||
@@ -5294,7 +5258,6 @@ export type Database = {
|
|||||||
assigned_at: string | null
|
assigned_at: string | null
|
||||||
assigned_profile: Json | null
|
assigned_profile: Json | null
|
||||||
assigned_to: string | null
|
assigned_to: string | null
|
||||||
content: Json | null
|
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
escalated: boolean | null
|
escalated: boolean | null
|
||||||
escalated_at: string | null
|
escalated_at: string | null
|
||||||
|
|||||||
@@ -322,14 +322,30 @@ export async function fetchSystemActivities(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch admin audit log (admin actions)
|
// Fetch admin audit log (admin actions)
|
||||||
|
// Note: Details are now in admin_audit_details table
|
||||||
const { data: auditLogs, error: auditError } = await supabase
|
const { data: auditLogs, error: auditError } = await supabase
|
||||||
.from('admin_audit_log')
|
.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 })
|
.order('created_at', { ascending: false })
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
if (!auditError && auditLogs) {
|
if (!auditError && auditLogs) {
|
||||||
for (const log of auditLogs) {
|
for (const log of auditLogs) {
|
||||||
|
// Convert relational details back to object format
|
||||||
|
const details: Record<string, any> = {};
|
||||||
|
if (log.admin_audit_details) {
|
||||||
|
for (const detail of log.admin_audit_details as any[]) {
|
||||||
|
details[detail.detail_key] = detail.detail_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
activities.push({
|
activities.push({
|
||||||
id: log.id,
|
id: log.id,
|
||||||
type: 'admin_action',
|
type: 'admin_action',
|
||||||
@@ -339,16 +355,24 @@ export async function fetchSystemActivities(
|
|||||||
details: {
|
details: {
|
||||||
action: log.action,
|
action: log.action,
|
||||||
target_user_id: log.target_user_id,
|
target_user_id: log.target_user_id,
|
||||||
details: log.details,
|
details,
|
||||||
} as AdminActionDetails,
|
} as AdminActionDetails,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch submission reviews (approved/rejected submissions)
|
// 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
|
const { data: submissions, error: submissionsError } = await supabase
|
||||||
.from('content_submissions')
|
.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)
|
.not('reviewed_at', 'is', null)
|
||||||
.in('status', ['approved', 'rejected', 'partially_approved'])
|
.in('status', ['approved', 'rejected', 'partially_approved'])
|
||||||
.order('reviewed_at', { ascending: false })
|
.order('reviewed_at', { ascending: false })
|
||||||
@@ -385,7 +409,12 @@ export async function fetchSystemActivities(
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const submission of submissions) {
|
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 submissionItem = itemsMap.get(submission.id);
|
||||||
const itemData = submissionItem?.item_data as any;
|
const itemData = submissionItem?.item_data as any;
|
||||||
|
|
||||||
@@ -394,7 +423,7 @@ export async function fetchSystemActivities(
|
|||||||
submission_id: submission.id,
|
submission_id: submission.id,
|
||||||
submission_type: submission.submission_type,
|
submission_type: submission.submission_type,
|
||||||
status: submission.status,
|
status: submission.status,
|
||||||
entity_name: contentData?.name,
|
entity_name: entityName,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enrich with photo-specific data for photo submissions
|
// Enrich with photo-specific data for photo submissions
|
||||||
@@ -600,7 +629,14 @@ export async function fetchSystemActivities(
|
|||||||
// 3. User bans/unbans from admin audit log
|
// 3. User bans/unbans from admin audit log
|
||||||
const { data: banActions, error: banError } = await supabase
|
const { data: banActions, error: banError } = await supabase
|
||||||
.from('admin_audit_log')
|
.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'])
|
.in('action', ['user_banned', 'user_unbanned'])
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
@@ -608,6 +644,15 @@ export async function fetchSystemActivities(
|
|||||||
if (!banError && banActions) {
|
if (!banError && banActions) {
|
||||||
for (const action of banActions) {
|
for (const action of banActions) {
|
||||||
const activityType = action.action === 'user_banned' ? 'user_banned' : 'user_unbanned';
|
const activityType = action.action === 'user_banned' ? 'user_banned' : 'user_unbanned';
|
||||||
|
|
||||||
|
// Convert relational details back to object format
|
||||||
|
const details: Record<string, any> = {};
|
||||||
|
if (action.admin_audit_details) {
|
||||||
|
for (const detail of action.admin_audit_details as any[]) {
|
||||||
|
details[detail.detail_key] = detail.detail_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
activities.push({
|
activities.push({
|
||||||
id: action.id,
|
id: action.id,
|
||||||
type: activityType,
|
type: activityType,
|
||||||
@@ -617,7 +662,7 @@ export async function fetchSystemActivities(
|
|||||||
details: {
|
details: {
|
||||||
user_id: action.target_user_id,
|
user_id: action.target_user_id,
|
||||||
action: action.action === 'user_banned' ? 'banned' : 'unbanned',
|
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,
|
} as AccountLifecycleDetails,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,7 +287,13 @@ export default function Profile() {
|
|||||||
// Fetch last 10 submissions with enriched data
|
// Fetch last 10 submissions with enriched data
|
||||||
let submissionsQuery = supabase
|
let submissionsQuery = supabase
|
||||||
.from('content_submissions')
|
.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)
|
.eq('user_id', userId)
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.limit(10);
|
.limit(10);
|
||||||
@@ -305,6 +311,12 @@ export default function Profile() {
|
|||||||
const enrichedSubmissions = await Promise.all((submissions || []).map(async (sub) => {
|
const enrichedSubmissions = await Promise.all((submissions || []).map(async (sub) => {
|
||||||
const enriched: any = { ...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
|
// For photo submissions, get photo count and preview
|
||||||
if (sub.submission_type === 'photo') {
|
if (sub.submission_type === 'photo') {
|
||||||
const { data: photoSubs } = await supabase
|
const { data: photoSubs } = await supabase
|
||||||
|
|||||||
@@ -24,7 +24,16 @@ export default function ErrorLookup() {
|
|||||||
// Search by partial or full request ID
|
// Search by partial or full request ID
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('request_metadata')
|
.from('request_metadata')
|
||||||
.select('*')
|
.select(`
|
||||||
|
*,
|
||||||
|
request_breadcrumbs(
|
||||||
|
timestamp,
|
||||||
|
category,
|
||||||
|
message,
|
||||||
|
level,
|
||||||
|
sequence_order
|
||||||
|
)
|
||||||
|
`)
|
||||||
.ilike('request_id', `${errorId}%`)
|
.ilike('request_id', `${errorId}%`)
|
||||||
.not('error_type', 'is', null)
|
.not('error_type', 'is', null)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|||||||
@@ -31,7 +31,16 @@ export default function ErrorMonitoring() {
|
|||||||
|
|
||||||
let query = supabase
|
let query = supabase
|
||||||
.from('request_metadata')
|
.from('request_metadata')
|
||||||
.select('*')
|
.select(`
|
||||||
|
*,
|
||||||
|
request_breadcrumbs(
|
||||||
|
timestamp,
|
||||||
|
category,
|
||||||
|
message,
|
||||||
|
level,
|
||||||
|
sequence_order
|
||||||
|
)
|
||||||
|
`)
|
||||||
.not('error_type', 'is', null)
|
.not('error_type', 'is', null)
|
||||||
.gte('created_at', `now() - interval '${dateMap[dateRange]}'`)
|
.gte('created_at', `now() - interval '${dateMap[dateRange]}'`)
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- PHASE 6: DROP JSONB COLUMNS (With View Dependencies Fixed)
|
||||||
|
-- ============================================================================
|
||||||
|
--
|
||||||
|
-- ⚠️⚠️⚠️ DANGER: THIS MIGRATION IS IRREVERSIBLE ⚠️⚠️⚠️
|
||||||
|
--
|
||||||
|
-- This migration drops all JSONB columns from production tables.
|
||||||
|
-- Once executed, there is NO WAY to recover the JSONB data without a backup.
|
||||||
|
--
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Log this critical operation
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE 'Starting Phase 6: Dropping JSONB columns';
|
||||||
|
RAISE NOTICE 'This operation is IRREVERSIBLE';
|
||||||
|
RAISE NOTICE 'Timestamp: %', NOW();
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 0: Drop views that depend on JSONB columns
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP VIEW IF EXISTS moderation_queue_with_entities CASCADE;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 1: Drop JSONB columns from audit tables
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- admin_audit_log.details → admin_audit_details table
|
||||||
|
ALTER TABLE admin_audit_log
|
||||||
|
DROP COLUMN IF EXISTS details;
|
||||||
|
|
||||||
|
COMMENT ON TABLE admin_audit_log IS 'Admin audit log (details migrated to admin_audit_details table)';
|
||||||
|
|
||||||
|
-- moderation_audit_log.metadata → moderation_audit_metadata table
|
||||||
|
ALTER TABLE moderation_audit_log
|
||||||
|
DROP COLUMN IF EXISTS metadata;
|
||||||
|
|
||||||
|
COMMENT ON TABLE moderation_audit_log IS 'Moderation audit log (metadata migrated to moderation_audit_metadata table)';
|
||||||
|
|
||||||
|
-- profile_audit_log.changes → profile_change_fields table
|
||||||
|
ALTER TABLE profile_audit_log
|
||||||
|
DROP COLUMN IF EXISTS changes;
|
||||||
|
|
||||||
|
COMMENT ON TABLE profile_audit_log IS 'Profile audit log (changes migrated to profile_change_fields table)';
|
||||||
|
|
||||||
|
-- item_edit_history.changes → item_change_fields table
|
||||||
|
ALTER TABLE item_edit_history
|
||||||
|
DROP COLUMN IF EXISTS changes;
|
||||||
|
|
||||||
|
COMMENT ON TABLE item_edit_history IS 'Item edit history (changes migrated to item_change_fields table)';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 2: Drop JSONB columns from request tracking
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- request_metadata.breadcrumbs → request_breadcrumbs table
|
||||||
|
ALTER TABLE request_metadata
|
||||||
|
DROP COLUMN IF EXISTS breadcrumbs;
|
||||||
|
|
||||||
|
-- request_metadata.environment_context
|
||||||
|
ALTER TABLE request_metadata
|
||||||
|
DROP COLUMN IF EXISTS environment_context;
|
||||||
|
|
||||||
|
COMMENT ON TABLE request_metadata IS 'Request metadata (breadcrumbs migrated to request_breadcrumbs table)';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 3: Drop JSONB columns from notification system
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- notification_logs.payload → notification_event_data table
|
||||||
|
ALTER TABLE notification_logs
|
||||||
|
DROP COLUMN IF EXISTS payload;
|
||||||
|
|
||||||
|
COMMENT ON TABLE notification_logs IS 'Notification logs (payload migrated to notification_event_data table)';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 4: Drop JSONB columns from moderation system
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- conflict_resolutions.conflict_details → conflict_detail_fields table
|
||||||
|
ALTER TABLE conflict_resolutions
|
||||||
|
DROP COLUMN IF EXISTS conflict_details;
|
||||||
|
|
||||||
|
COMMENT ON TABLE conflict_resolutions IS 'Conflict resolutions (details migrated to conflict_detail_fields table)';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 5: Drop JSONB columns from contact system
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- contact_email_threads.metadata
|
||||||
|
ALTER TABLE contact_email_threads
|
||||||
|
DROP COLUMN IF EXISTS metadata;
|
||||||
|
|
||||||
|
-- contact_submissions.submitter_profile_data → FK to profiles table
|
||||||
|
ALTER TABLE contact_submissions
|
||||||
|
DROP COLUMN IF EXISTS submitter_profile_data;
|
||||||
|
|
||||||
|
COMMENT ON TABLE contact_submissions IS 'Contact submissions (profile data accessed via FK to profiles table)';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 6: Drop JSONB columns from content system
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- content_submissions.content → submission_metadata table
|
||||||
|
ALTER TABLE content_submissions
|
||||||
|
DROP COLUMN IF EXISTS content;
|
||||||
|
|
||||||
|
COMMENT ON TABLE content_submissions IS 'Content submissions (metadata migrated to submission_metadata table)';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 7: Drop JSONB columns from review system
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- reviews.photos → review_photos table
|
||||||
|
ALTER TABLE reviews
|
||||||
|
DROP COLUMN IF EXISTS photos;
|
||||||
|
|
||||||
|
COMMENT ON TABLE reviews IS 'Reviews (photos migrated to review_photos table)';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 8: Recreate views without JSONB columns
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Recreate moderation_queue_with_entities view WITHOUT content column
|
||||||
|
CREATE VIEW moderation_queue_with_entities AS
|
||||||
|
SELECT
|
||||||
|
cs.id,
|
||||||
|
cs.submission_type,
|
||||||
|
cs.status,
|
||||||
|
|
||||||
|
-- Temporal fields (with backward compatibility alias)
|
||||||
|
cs.submitted_at AS created_at, -- Primary alias for frontend
|
||||||
|
cs.submitted_at, -- Also expose for semantic accuracy
|
||||||
|
cs.reviewed_at,
|
||||||
|
cs.assigned_at,
|
||||||
|
cs.escalated_at,
|
||||||
|
|
||||||
|
-- User relationships
|
||||||
|
cs.user_id as submitter_id,
|
||||||
|
cs.reviewer_id as reviewed_by,
|
||||||
|
cs.assigned_to,
|
||||||
|
cs.locked_until,
|
||||||
|
|
||||||
|
-- Flags and metadata
|
||||||
|
cs.escalated,
|
||||||
|
cs.escalation_reason,
|
||||||
|
cs.reviewer_notes,
|
||||||
|
cs.is_test_data,
|
||||||
|
-- NOTE: content column removed - use submission_metadata table instead
|
||||||
|
|
||||||
|
-- Submitter profile (matches frontend expectations)
|
||||||
|
CASE
|
||||||
|
WHEN sp.id IS NOT NULL THEN
|
||||||
|
jsonb_build_object(
|
||||||
|
'user_id', sp.user_id,
|
||||||
|
'username', sp.username,
|
||||||
|
'display_name', sp.display_name,
|
||||||
|
'avatar_url', sp.avatar_url
|
||||||
|
)
|
||||||
|
ELSE NULL
|
||||||
|
END as submitter_profile,
|
||||||
|
|
||||||
|
-- Reviewer profile
|
||||||
|
CASE
|
||||||
|
WHEN rp.id IS NOT NULL THEN
|
||||||
|
jsonb_build_object(
|
||||||
|
'user_id', rp.user_id,
|
||||||
|
'username', rp.username,
|
||||||
|
'display_name', rp.display_name,
|
||||||
|
'avatar_url', rp.avatar_url
|
||||||
|
)
|
||||||
|
ELSE NULL
|
||||||
|
END as reviewer_profile,
|
||||||
|
|
||||||
|
-- Assigned moderator profile
|
||||||
|
CASE
|
||||||
|
WHEN ap.id IS NOT NULL THEN
|
||||||
|
jsonb_build_object(
|
||||||
|
'user_id', ap.user_id,
|
||||||
|
'username', ap.username,
|
||||||
|
'display_name', ap.display_name,
|
||||||
|
'avatar_url', ap.avatar_url
|
||||||
|
)
|
||||||
|
ELSE NULL
|
||||||
|
END as assigned_profile,
|
||||||
|
|
||||||
|
-- Submission items with entity data
|
||||||
|
(
|
||||||
|
SELECT jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'id', si.id,
|
||||||
|
'submission_id', si.submission_id,
|
||||||
|
'item_type', si.item_type,
|
||||||
|
'item_data_id', si.item_data_id,
|
||||||
|
'action_type', si.action_type,
|
||||||
|
'status', si.status,
|
||||||
|
'order_index', si.order_index,
|
||||||
|
'depends_on', si.depends_on,
|
||||||
|
'approved_entity_id', si.approved_entity_id,
|
||||||
|
'rejection_reason', si.rejection_reason,
|
||||||
|
'created_at', si.created_at,
|
||||||
|
'updated_at', si.updated_at,
|
||||||
|
'entity_data', get_submission_item_entity_data(si.item_type, si.item_data_id)
|
||||||
|
)
|
||||||
|
ORDER BY si.order_index
|
||||||
|
)
|
||||||
|
FROM submission_items si
|
||||||
|
WHERE si.submission_id = cs.id
|
||||||
|
) as submission_items
|
||||||
|
|
||||||
|
FROM content_submissions cs
|
||||||
|
LEFT JOIN profiles sp ON sp.user_id = cs.user_id
|
||||||
|
LEFT JOIN profiles rp ON rp.user_id = cs.reviewer_id
|
||||||
|
LEFT JOIN profiles ap ON ap.user_id = cs.assigned_to;
|
||||||
|
|
||||||
|
COMMENT ON VIEW moderation_queue_with_entities IS
|
||||||
|
'Optimized view for moderation queue with pre-joined profiles and entity data. Content metadata moved to submission_metadata table.';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 9: Verify no JSONB columns remain (except approved)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
jsonb_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO jsonb_count
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND data_type = 'jsonb'
|
||||||
|
AND table_name NOT IN (
|
||||||
|
'admin_settings', -- System config (approved)
|
||||||
|
'user_preferences', -- UI config (approved)
|
||||||
|
'user_notification_preferences', -- Notification config (approved)
|
||||||
|
'notification_channels', -- Channel config (approved)
|
||||||
|
'test_data_registry', -- Test metadata (approved)
|
||||||
|
'entity_versions_archive', -- Archive table (approved)
|
||||||
|
'historical_parks', -- Historical data (approved)
|
||||||
|
'historical_rides' -- Historical data (approved)
|
||||||
|
);
|
||||||
|
|
||||||
|
IF jsonb_count > 0 THEN
|
||||||
|
RAISE WARNING 'Found % unexpected JSONB columns still in database', jsonb_count;
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'SUCCESS: All production JSONB columns have been dropped';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 10: Update database documentation
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
COMMENT ON DATABASE postgres IS 'ThrillWiki Database - JSONB elimination completed';
|
||||||
|
|
||||||
|
-- Log completion
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE 'Phase 6 Complete: All JSONB columns dropped';
|
||||||
|
RAISE NOTICE 'Timestamp: %', NOW();
|
||||||
|
RAISE NOTICE 'Next steps: Update TypeScript types and documentation';
|
||||||
|
END $$;
|
||||||
Reference in New Issue
Block a user