From bcba0a4f0c34248bc7fd65ae54b3560c9999e0e7 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:41:42 +0000
Subject: [PATCH] Add documentation to files
---
docs/ARCHITECTURE_OVERVIEW.md | 167 +++++++++
docs/DATABASE_ARCHITECTURE.md | 636 ++++++++++++++++++++++++++++++++
docs/DEPLOYMENT.md | 446 +++++++++++++++++++++++
docs/FRONTEND_ARCHITECTURE.md | 658 ++++++++++++++++++++++++++++++++++
docs/TESTING_GUIDE.md | 501 ++++++++++++++++++++++++++
docs/TROUBLESHOOTING.md | 614 +++++++++++++++++++++++++++++++
6 files changed, 3022 insertions(+)
create mode 100644 docs/ARCHITECTURE_OVERVIEW.md
create mode 100644 docs/DATABASE_ARCHITECTURE.md
create mode 100644 docs/DEPLOYMENT.md
create mode 100644 docs/FRONTEND_ARCHITECTURE.md
create mode 100644 docs/TESTING_GUIDE.md
create mode 100644 docs/TROUBLESHOOTING.md
diff --git a/docs/ARCHITECTURE_OVERVIEW.md b/docs/ARCHITECTURE_OVERVIEW.md
new file mode 100644
index 00000000..2142b509
--- /dev/null
+++ b/docs/ARCHITECTURE_OVERVIEW.md
@@ -0,0 +1,167 @@
+# ThrillWiki: System Architecture Overview
+
+## 🎯 Executive Summary
+
+**ThrillWiki** is a comprehensive theme park and ride tracking platform built with React, TypeScript, Vite, Tailwind CSS, Supabase (PostgreSQL), and CloudFlare Images. The application implements a **moderation-first architecture** where all user-generated content flows through a sophisticated approval queue before going live.
+
+### Core Technology Stack
+- **Frontend**: React 18.3, TypeScript, Vite, Tailwind CSS, shadcn/ui
+- **Backend**: Supabase (PostgreSQL 15), Edge Functions (Deno)
+- **Storage**: CloudFlare Images (direct upload API)
+- **Notifications**: Novu Cloud (multi-channel)
+- **State Management**: React Query (TanStack Query), React Hook Form, State Machines
+- **Authentication**: Supabase Auth with MFA (TOTP), OAuth (Google, Discord)
+
+---
+
+## 📐 High-Level Architecture
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ USERS (Web Browser) │
+└───────────────────────────┬─────────────────────────────────────┘
+ │
+ ▼
+┌───────────────────────────────────────────────────────────────┐
+│ REACT FRONTEND (SPA) │
+│ - React Router (client-side routing) │
+│ - React Query (data caching/sync) │
+│ - shadcn/ui + Tailwind CSS │
+│ - State Machines (moderation, auth flows) │
+└───────────────────────┬───────────────────────────────────────┘
+ │
+ ┌───────────────┼───────────────┬──────────────┐
+ ▼ ▼ ▼ ▼
+┌────────────┐ ┌────────────┐ ┌──────────┐ ┌──────────────┐
+│ Supabase │ │ CloudFlare │ │ Novu │ │ Edge │
+│ Database │ │ Images │ │ Cloud │ │ Functions │
+│ PostgreSQL │ │ (direct) │ │(webhooks)│ │ (Deno) │
+└────────────┘ └────────────┘ └──────────┘ └──────────────┘
+```
+
+---
+
+## 🔄 Data Flow Architecture
+
+### Entity Creation/Edit Flow (CRITICAL PATTERN)
+
+```
+1. User fills form → 2. Submission to moderation queue →
+3. Moderator reviews → 4. Edge function writes DB (service role) →
+5. Versioning trigger → 6. Entity live on site
+```
+
+**Rule #1: NEVER write directly to entity tables**
+
+```typescript
+// ❌ WRONG - Bypasses moderation, no versioning, no audit trail
+await supabase.from('parks').insert(parkData);
+
+// ✅ CORRECT - Goes through moderation queue
+await submitParkCreation(parkData, user.id);
+```
+
+### Authentication Flow
+
+```
+1. User signs in (password/OAuth) → 2. Session established (AAL1) →
+3. Check MFA enrollment → 4. If enrolled: MFA challenge (AAL2) →
+5. Full access granted
+```
+
+---
+
+## 🗄️ Core Principles
+
+1. **NO JSONB for relational data** - All data properly normalized
+2. **Metric-first storage** - All measurements in metric (km/h, m, cm, kg)
+3. **Relational versioning** - Full history without JSONB
+4. **RLS everywhere** - Row-Level Security on all tables
+5. **Moderation-first** - Direct writes blocked, all through approval flow
+6. **Calendar dates** - DATE type for dates, not TIMESTAMP (timezone-safe)
+7. **Type safety** - TypeScript throughout, minimal `any` usage
+8. **State machines** - Complex workflows managed by state machines
+
+---
+
+## 📚 Documentation Structure
+
+### Architecture & Design
+- **[ARCHITECTURE_OVERVIEW.md](./ARCHITECTURE_OVERVIEW.md)** (this file) - High-level system overview
+- **[DATABASE_ARCHITECTURE.md](./DATABASE_ARCHITECTURE.md)** - Complete database schema and RLS patterns
+- **[FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md)** - React components, routing, state management
+- **[AUTHENTICATION.md](./AUTHENTICATION.md)** - Auth methods, MFA, roles, session management
+
+### Core Systems
+- **[SUBMISSION_FLOW.md](./SUBMISSION_FLOW.md)** - Entity submission and moderation workflow
+- **[IMAGE_UPLOAD_SYSTEM.md](./IMAGE_UPLOAD_SYSTEM.md)** - CloudFlare Images integration
+- **[UNIT_SYSTEM.md](./UNIT_SYSTEM.md)** - Metric storage and display conversion
+- **[DATE_HANDLING.md](./DATE_HANDLING.md)** - Timezone-safe date handling
+- **[NOTIFICATION_SYSTEM.md](./NOTIFICATION_SYSTEM.md)** - Novu integration
+- **[versioning/README.md](./versioning/README.md)** - Versioning system overview
+
+### Implementation Details
+- **[PHASE_4_IMPLEMENTATION.md](./PHASE_4_IMPLEMENTATION.md)** - State machine integration
+- **[TYPE_SAFETY_MIGRATION.md](./TYPE_SAFETY_MIGRATION.md)** - TypeScript improvements
+- **[JSONB_ELIMINATION.md](./JSONB_ELIMINATION.md)** - Database normalization journey
+
+### Operations
+- **[TESTING_GUIDE.md](./TESTING_GUIDE.md)** - Testing procedures and checklists
+- **[DEPLOYMENT.md](./DEPLOYMENT.md)** - Deployment and environment setup
+- **[TROUBLESHOOTING.md](./TROUBLESHOOTING.md)** - Common issues and solutions
+
+---
+
+## 🚦 System Status
+
+### ✅ Production Ready
+1. Authentication & Authorization (incl. MFA)
+2. Moderation Queue & State Machine
+3. Submission Flow (parks, rides, companies, models)
+4. Versioning System (fully relational)
+5. Image Upload (CloudFlare direct upload)
+6. Unit System (metric storage, display conversion)
+7. Date Handling (timezone-safe)
+8. Notification System (Novu integration)
+9. Lock Management (15-min locks with extension)
+10. Type Safety (minimal `any` usage)
+
+### 📋 Remaining Work
+- Complete type safety migration (28 `as any` remaining)
+- Unit validation in editors
+- Integration testing
+- Performance optimization
+- User documentation
+
+---
+
+## 🔑 Quick Start
+
+```bash
+# Install dependencies
+npm install
+
+# Set up environment variables (see DEPLOYMENT.md)
+cp .env.example .env
+
+# Run development server
+npm run dev
+
+# Build for production
+npm run build
+```
+
+---
+
+## 📞 Support
+
+For questions or issues:
+1. Check relevant documentation file
+2. Review code comments and types
+3. Create GitHub issue with details
+
+---
+
+**Last Updated:** 2025-01-20
+**Version:** 1.0.0
+**Status:** 🟢 Production Ready
diff --git a/docs/DATABASE_ARCHITECTURE.md b/docs/DATABASE_ARCHITECTURE.md
new file mode 100644
index 00000000..c1c61af0
--- /dev/null
+++ b/docs/DATABASE_ARCHITECTURE.md
@@ -0,0 +1,636 @@
+# Database Architecture
+
+Complete documentation of ThrillWiki's PostgreSQL database schema, Row-Level Security policies, and design patterns.
+
+---
+
+## Core Principles
+
+1. **NO JSONB for relational data** - All data properly normalized
+2. **Metric-first storage** - All measurements in metric (km/h, m, cm, kg)
+3. **Relational versioning** - Full history without JSONB
+4. **RLS everywhere** - Row-Level Security on all tables
+5. **Moderation-first** - Direct writes blocked, all through approval flow
+
+---
+
+## Primary Entity Tables
+
+### parks
+
+Public-facing theme park/amusement park entities.
+
+```sql
+CREATE TABLE parks (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ description TEXT,
+ park_type TEXT CHECK (park_type IN ('theme_park', 'amusement_park', 'water_park', 'family_entertainment_center')),
+ status TEXT CHECK (status IN ('operating', 'closed', 'seasonal', 'construction')),
+
+ -- Dates with precision
+ opening_date DATE,
+ closing_date DATE,
+ opening_date_precision TEXT CHECK (opening_date_precision IN ('day', 'month', 'year')),
+ closing_date_precision TEXT CHECK (closing_date_precision IN ('day', 'month', 'year')),
+
+ -- Relations
+ location_id UUID REFERENCES locations(id),
+ operator_id UUID REFERENCES companies(id),
+ property_owner_id UUID REFERENCES companies(id),
+
+ -- Contact
+ website_url TEXT,
+ phone TEXT,
+ email TEXT,
+
+ -- Images (CloudFlare IDs)
+ banner_image_url TEXT,
+ banner_image_id TEXT,
+ card_image_url TEXT,
+ card_image_id TEXT,
+
+ -- Computed stats
+ ride_count INTEGER DEFAULT 0,
+ coaster_count INTEGER DEFAULT 0,
+ average_rating NUMERIC(3,2),
+ review_count INTEGER DEFAULT 0,
+
+ -- View tracking
+ view_count_7d INTEGER DEFAULT 0,
+ view_count_30d INTEGER DEFAULT 0,
+ view_count_all INTEGER DEFAULT 0,
+
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Public READ, direct INSERT/UPDATE blocked
+CREATE POLICY "Public read access" ON parks FOR SELECT USING (true);
+CREATE POLICY "Deny direct inserts" ON parks FOR INSERT WITH CHECK (false);
+CREATE POLICY "Deny direct updates" ON parks FOR UPDATE USING (false);
+```
+
+### rides
+
+Roller coasters, flat rides, water rides, dark rides, etc.
+
+```sql
+CREATE TABLE rides (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ description TEXT,
+ category TEXT CHECK (category IN ('roller_coaster', 'flat_ride', 'water_ride', 'dark_ride', 'transport', 'show', 'other')),
+ status TEXT CHECK (status IN ('operating', 'closed', 'sbno', 'relocated', 'demolished')),
+
+ -- Relations
+ park_id UUID REFERENCES parks(id) NOT NULL,
+ manufacturer_id UUID REFERENCES companies(id),
+ designer_id UUID REFERENCES companies(id),
+ ride_model_id UUID REFERENCES ride_models(id),
+
+ -- Dates with precision
+ opening_date DATE,
+ closing_date DATE,
+ opening_date_precision TEXT,
+ closing_date_precision TEXT,
+
+ -- Roller coaster stats (ALWAYS METRIC)
+ max_speed_kmh NUMERIC(6,2),
+ max_height_meters NUMERIC(6,2),
+ length_meters NUMERIC(8,2),
+ drop_height_meters NUMERIC(6,2),
+ inversions INTEGER,
+ max_g_force NUMERIC(4,2),
+ duration_seconds INTEGER,
+ capacity_per_hour INTEGER,
+ height_requirement_cm NUMERIC(5,2),
+ age_requirement INTEGER,
+
+ coaster_type TEXT,
+ seating_type TEXT,
+ intensity_level TEXT CHECK (intensity_level IN ('low', 'moderate', 'high', 'extreme')),
+
+ -- Images
+ banner_image_url TEXT,
+ banner_image_id TEXT,
+ card_image_url TEXT,
+ card_image_id TEXT,
+
+ -- Computed stats
+ average_rating NUMERIC(3,2),
+ review_count INTEGER DEFAULT 0,
+ view_count_7d INTEGER DEFAULT 0,
+ view_count_30d INTEGER DEFAULT 0,
+ view_count_all INTEGER DEFAULT 0,
+
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Public READ, direct writes blocked
+CREATE POLICY "Public read access" ON rides FOR SELECT USING (true);
+CREATE POLICY "Deny direct inserts" ON rides FOR INSERT WITH CHECK (false);
+CREATE POLICY "Deny direct updates" ON rides FOR UPDATE USING (false);
+```
+
+### companies
+
+Manufacturers, designers, operators, property owners.
+
+```sql
+CREATE TABLE companies (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ description TEXT,
+ company_type TEXT CHECK (company_type IN ('manufacturer', 'designer', 'operator', 'property_owner')),
+ person_type TEXT CHECK (person_type IN ('company', 'individual', 'firm', 'organization')),
+
+ founded_date DATE,
+ founded_date_precision TEXT,
+ founded_year INTEGER, -- Legacy, prefer founded_date
+
+ headquarters_location TEXT,
+ website_url TEXT,
+
+ -- Images
+ logo_url TEXT,
+ banner_image_url TEXT,
+ banner_image_id TEXT,
+ card_image_url TEXT,
+ card_image_id TEXT,
+
+ -- Computed stats
+ average_rating NUMERIC(3,2),
+ review_count INTEGER DEFAULT 0,
+ view_count_7d INTEGER DEFAULT 0,
+ view_count_30d INTEGER DEFAULT 0,
+ view_count_all INTEGER DEFAULT 0,
+
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Same as parks/rides
+```
+
+### ride_models
+
+Manufacturer's ride models (e.g., "Inverted Coaster", "Flying Coaster").
+
+```sql
+CREATE TABLE ride_models (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ manufacturer_id UUID REFERENCES companies(id) NOT NULL,
+ category TEXT,
+ ride_type TEXT,
+ description TEXT,
+
+ banner_image_url TEXT,
+ banner_image_id TEXT,
+ card_image_url TEXT,
+ card_image_id TEXT,
+
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Same as other entities
+```
+
+---
+
+## Relational Data Tables (NO JSONB!)
+
+### ride_coaster_stats
+
+Normalized coaster statistics (replaces old JSONB column).
+
+```sql
+CREATE TABLE ride_coaster_stats (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ ride_id UUID REFERENCES rides(id) NOT NULL,
+ stat_name TEXT NOT NULL, -- 'max_speed', 'height', 'length', etc.
+ stat_value NUMERIC NOT NULL, -- ALWAYS METRIC
+ unit TEXT NOT NULL, -- 'km/h', 'm', 'cm', 'kg'
+ category TEXT, -- Grouping for UI
+ description TEXT,
+ display_order INTEGER,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Public READ, moderators manage
+CREATE POLICY "Public read" ON ride_coaster_stats FOR SELECT USING (true);
+CREATE POLICY "Moderators manage" ON ride_coaster_stats FOR ALL
+ USING (is_moderator(auth.uid()));
+```
+
+### ride_technical_specifications
+
+Normalized technical specs (track type, launch system, etc.).
+
+```sql
+CREATE TABLE ride_technical_specifications (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ ride_id UUID REFERENCES rides(id) NOT NULL,
+ spec_name TEXT NOT NULL,
+ spec_value TEXT NOT NULL,
+ spec_type TEXT CHECK (spec_type IN ('string', 'number', 'boolean', 'date')),
+ category TEXT,
+ unit TEXT,
+ display_order INTEGER,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Same as ride_coaster_stats
+```
+
+### locations
+
+Geographic locations for parks.
+
+```sql
+CREATE TABLE locations (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT,
+ country TEXT NOT NULL,
+ state_province TEXT,
+ city TEXT,
+ postal_code TEXT,
+ latitude NUMERIC(10,7),
+ longitude NUMERIC(10,7),
+ timezone TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Public READ, moderators INSERT/UPDATE
+```
+
+---
+
+## User & Profile Tables
+
+### profiles
+
+Public-facing user data (privacy-filtered).
+
+```sql
+CREATE TABLE profiles (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID REFERENCES auth.users(id) UNIQUE NOT NULL,
+ username TEXT UNIQUE NOT NULL,
+ display_name TEXT,
+ bio TEXT,
+ avatar_url TEXT,
+ avatar_image_id TEXT,
+
+ -- Privacy
+ preferred_pronouns TEXT,
+ show_pronouns BOOLEAN DEFAULT false,
+ privacy_level TEXT CHECK (privacy_level IN ('public', 'friends', 'private')) DEFAULT 'public',
+
+ -- Preferences
+ timezone TEXT,
+ preferred_language TEXT,
+ theme_preference TEXT CHECK (theme_preference IN ('light', 'dark', 'system')) DEFAULT 'system',
+
+ -- Location
+ location_id UUID REFERENCES locations(id),
+ personal_location TEXT, -- User-entered
+ home_park_id UUID REFERENCES parks(id),
+
+ date_of_birth DATE,
+
+ -- Stats
+ ride_count INTEGER DEFAULT 0,
+ coaster_count INTEGER DEFAULT 0,
+ park_count INTEGER DEFAULT 0,
+ review_count INTEGER DEFAULT 0,
+ reputation_score INTEGER DEFAULT 0,
+
+ -- Moderation
+ banned BOOLEAN DEFAULT false,
+ deactivated BOOLEAN DEFAULT false,
+ deactivated_at TIMESTAMPTZ,
+ deactivation_reason TEXT,
+
+ oauth_provider TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Privacy-filtered via get_filtered_profile() function
+CREATE POLICY "Privacy filtered" ON profiles FOR SELECT
+ USING (get_filtered_profile(user_id, auth.uid()) IS NOT NULL);
+```
+
+### user_roles
+
+Role-based access control (ONE role per user).
+
+```sql
+CREATE TYPE app_role AS ENUM ('user', 'moderator', 'admin', 'superuser');
+
+CREATE TABLE user_roles (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID REFERENCES auth.users(id) UNIQUE NOT NULL,
+ role app_role NOT NULL DEFAULT 'user',
+ granted_by UUID REFERENCES auth.users(id),
+ granted_at TIMESTAMPTZ DEFAULT NOW(),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Moderators+ can read, admins+ can write (with MFA)
+CREATE POLICY "Moderators read" ON user_roles FOR SELECT
+ USING (is_moderator(auth.uid()));
+
+CREATE POLICY "Admins write" ON user_roles FOR ALL
+ USING (is_admin(auth.uid()) AND (NOT has_mfa_enabled(auth.uid()) OR has_aal2()));
+```
+
+### user_ride_credits
+
+User's tracked rides.
+
+```sql
+CREATE TABLE user_ride_credits (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID REFERENCES auth.users(id) NOT NULL,
+ ride_id UUID REFERENCES rides(id) NOT NULL,
+ first_ride_date DATE,
+ last_ride_date DATE,
+ ride_count INTEGER DEFAULT 1,
+ sort_order INTEGER, -- For drag-drop sorting
+ personal_notes TEXT,
+ personal_rating NUMERIC(2,1) CHECK (personal_rating >= 1 AND personal_rating <= 5),
+ personal_photo_id TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
+
+ UNIQUE(user_id, ride_id)
+);
+
+-- RLS: Users manage own credits
+CREATE POLICY "Users manage own" ON user_ride_credits FOR ALL
+ USING (auth.uid() = user_id);
+```
+
+---
+
+## Moderation System Tables
+
+### content_submissions
+
+Main moderation queue.
+
+```sql
+CREATE TABLE content_submissions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID REFERENCES auth.users(id), -- Nullable after anonymization
+ submission_type TEXT CHECK (submission_type IN ('park', 'ride', 'company', 'ride_model', 'photo')),
+
+ -- MINIMAL metadata only (NOT full form data!)
+ content JSONB NOT NULL,
+
+ status TEXT CHECK (status IN ('pending', 'partially_approved', 'approved', 'rejected')) DEFAULT 'pending',
+ approval_mode TEXT CHECK (approval_mode IN ('full', 'selective')) DEFAULT 'full',
+
+ reviewer_id UUID REFERENCES auth.users(id),
+ reviewer_notes TEXT,
+
+ submitted_at TIMESTAMPTZ DEFAULT NOW(),
+ reviewed_at TIMESTAMPTZ,
+ resolved_at TIMESTAMPTZ,
+
+ -- Lock management (15-minute locks)
+ assigned_to UUID REFERENCES auth.users(id),
+ assigned_at TIMESTAMPTZ,
+ locked_until TIMESTAMPTZ,
+ review_count INTEGER DEFAULT 0,
+ first_reviewed_at TIMESTAMPTZ,
+
+ -- Escalation
+ escalated BOOLEAN DEFAULT false,
+ escalated_by UUID REFERENCES auth.users(id),
+ escalated_at TIMESTAMPTZ,
+ escalation_reason TEXT,
+
+ original_submission_id UUID REFERENCES content_submissions(id),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Users see own, moderators see all (with AAL2 if MFA enrolled)
+CREATE POLICY "Users see own" ON content_submissions FOR SELECT
+ USING (auth.uid() = user_id);
+
+CREATE POLICY "Moderators see all" ON content_submissions FOR SELECT
+ USING (is_moderator(auth.uid()) AND (NOT has_mfa_enabled(auth.uid()) OR has_aal2()));
+```
+
+### submission_items
+
+Actual submission data (normalized).
+
+```sql
+CREATE TABLE submission_items (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ submission_id UUID REFERENCES content_submissions(id) NOT NULL,
+ item_type TEXT NOT NULL,
+ action_type TEXT CHECK (action_type IN ('create', 'edit', 'delete')) NOT NULL,
+
+ item_data JSONB NOT NULL, -- NEW data for this item
+ original_data JSONB, -- OLD data for edits
+
+ status TEXT CHECK (status IN ('pending', 'approved', 'rejected')) DEFAULT 'pending',
+ order_index INTEGER NOT NULL,
+ depends_on UUID REFERENCES submission_items(id), -- Dependency chain
+
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- RLS: Same as content_submissions
+```
+
+### photo_submissions & photo_submission_items
+
+Special handling for photo uploads.
+
+```sql
+CREATE TABLE photo_submissions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ submission_id UUID REFERENCES content_submissions(id) NOT NULL,
+ entity_type TEXT NOT NULL,
+ entity_id UUID NOT NULL,
+ parent_id UUID, -- Optional grouping
+ title TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE photo_submission_items (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ photo_submission_id UUID REFERENCES photo_submissions(id) NOT NULL,
+ cloudflare_image_id TEXT NOT NULL,
+ cloudflare_image_url TEXT NOT NULL,
+ title TEXT,
+ caption TEXT,
+ date_taken DATE,
+ date_taken_precision TEXT,
+ filename TEXT,
+ mime_type TEXT,
+ file_size BIGINT,
+ order_index INTEGER,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+```
+
+---
+
+## Versioning System Tables
+
+Each entity has a corresponding `_versions` table with full relational data.
+
+### park_versions (example)
+
+```sql
+CREATE TABLE park_versions (
+ version_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ park_id UUID REFERENCES parks(id) NOT NULL,
+ version_number INTEGER NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ created_by UUID REFERENCES auth.users(id),
+ change_type version_change_type CHECK (change_type IN ('created', 'updated', 'deleted')),
+ change_reason TEXT,
+ submission_id UUID REFERENCES content_submissions(id),
+ is_current BOOLEAN DEFAULT true,
+
+ -- All park fields replicated (RELATIONAL, not JSONB!)
+ name TEXT,
+ slug TEXT,
+ description TEXT,
+ park_type TEXT,
+ status TEXT,
+ opening_date DATE,
+ closing_date DATE,
+ opening_date_precision TEXT,
+ closing_date_precision TEXT,
+ location_id UUID,
+ operator_id UUID,
+ property_owner_id UUID,
+ website_url TEXT,
+ phone TEXT,
+ email TEXT,
+ banner_image_url TEXT,
+ banner_image_id TEXT,
+ card_image_url TEXT,
+ card_image_id TEXT,
+
+ UNIQUE(park_id, version_number)
+);
+
+CREATE INDEX idx_park_versions_current ON park_versions(park_id, is_current) WHERE is_current = true;
+CREATE INDEX idx_park_versions_created ON park_versions(created_at DESC);
+```
+
+Similar tables exist for: `ride_versions`, `company_versions`, `ride_model_versions`.
+
+---
+
+## Row-Level Security Patterns
+
+### Pattern 1: Public Read, Moderation Write
+
+```sql
+CREATE POLICY "Public read access" ON {table} FOR SELECT USING (true);
+CREATE POLICY "Deny direct inserts" ON {table} FOR INSERT WITH CHECK (false);
+CREATE POLICY "Deny direct updates" ON {table} FOR UPDATE USING (false);
+```
+
+### Pattern 2: User-Scoped Data
+
+```sql
+CREATE POLICY "Users manage own" ON {table} FOR ALL
+ USING (auth.uid() = user_id);
+```
+
+### Pattern 3: Privacy-Filtered
+
+```sql
+CREATE POLICY "Privacy filtered" ON profiles FOR SELECT
+ USING (get_filtered_profile(user_id, auth.uid()) IS NOT NULL);
+```
+
+### Pattern 4: Moderator+ Access with MFA
+
+```sql
+CREATE POLICY "Moderators can manage" ON {table} FOR ALL
+ USING (
+ is_moderator(auth.uid()) AND
+ (NOT has_mfa_enabled(auth.uid()) OR has_aal2())
+ );
+```
+
+---
+
+## Helper Functions
+
+### is_moderator()
+
+```sql
+CREATE FUNCTION is_moderator(user_id UUID) RETURNS BOOLEAN AS $$
+ SELECT EXISTS (
+ SELECT 1 FROM user_roles
+ WHERE user_id = $1 AND role IN ('moderator', 'admin', 'superuser')
+ );
+$$ LANGUAGE SQL STABLE SECURITY DEFINER;
+```
+
+### get_filtered_profile()
+
+Implements privacy filtering based on privacy_level.
+
+```sql
+CREATE FUNCTION get_filtered_profile(
+ profile_user_id UUID,
+ requesting_user_id UUID
+) RETURNS profiles AS $$
+ -- Complex logic to filter based on privacy settings
+$$ LANGUAGE SQL STABLE SECURITY DEFINER;
+```
+
+---
+
+## Indexing Strategy
+
+```sql
+-- Entity lookups
+CREATE INDEX idx_parks_slug ON parks(slug);
+CREATE INDEX idx_rides_park ON rides(park_id);
+CREATE INDEX idx_rides_status ON rides(status);
+
+-- Moderation queue
+CREATE INDEX idx_submissions_status ON content_submissions(status) WHERE status IN ('pending', 'partially_approved');
+CREATE INDEX idx_submissions_assigned ON content_submissions(assigned_to, locked_until);
+
+-- View tracking
+CREATE INDEX idx_page_views_entity ON entity_page_views(entity_type, entity_id, viewed_at DESC);
+
+-- User credits
+CREATE INDEX idx_ride_credits_user ON user_ride_credits(user_id, sort_order);
+```
+
+---
+
+**See Also:**
+- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - How frontend interacts with database
+- [SUBMISSION_FLOW.md](./SUBMISSION_FLOW.md) - How submissions flow through moderation
+- [versioning/SCHEMA.md](./versioning/SCHEMA.md) - Detailed versioning system
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md
new file mode 100644
index 00000000..7bd2f136
--- /dev/null
+++ b/docs/DEPLOYMENT.md
@@ -0,0 +1,446 @@
+# Deployment Guide
+
+Complete guide to deploying and configuring ThrillWiki.
+
+---
+
+## Prerequisites
+
+- Node.js 18+ and npm/pnpm
+- Supabase project (or Lovable Cloud)
+- CloudFlare Images account
+- Novu Cloud account (optional, for notifications)
+
+---
+
+## Environment Variables
+
+### Frontend (.env)
+
+```bash
+# Supabase
+VITE_SUPABASE_URL=https://[project-ref].supabase.co
+VITE_SUPABASE_ANON_KEY=[anon_key]
+
+# CloudFlare Images
+VITE_CLOUDFLARE_ACCOUNT_HASH=[account_hash]
+
+# Novu (optional)
+VITE_NOVU_APPLICATION_IDENTIFIER=[app_id]
+VITE_NOVU_SOCKET_URL=wss://ws.novu.co
+VITE_NOVU_API_URL=https://api.novu.co
+```
+
+### Backend (Supabase Secrets)
+
+Set these in Supabase Dashboard → Project Settings → Secrets:
+
+```bash
+# CloudFlare Images
+CLOUDFLARE_ACCOUNT_ID=[account_id]
+CLOUDFLARE_IMAGES_API_TOKEN=[api_token]
+
+# Novu (optional)
+NOVU_API_KEY=[api_key]
+```
+
+---
+
+## Setup Instructions
+
+### 1. Clone Repository
+
+```bash
+git clone https://github.com/your-org/thrillwiki.git
+cd thrillwiki
+```
+
+### 2. Install Dependencies
+
+```bash
+npm install
+# or
+pnpm install
+```
+
+### 3. Configure Environment
+
+```bash
+cp .env.example .env
+# Edit .env with your values
+```
+
+### 4. Database Setup
+
+The database migrations are already applied in Supabase. If setting up a new project:
+
+```bash
+# Install Supabase CLI
+npm install -g supabase
+
+# Link to your project
+supabase link --project-ref [project-ref]
+
+# Push migrations
+supabase db push
+```
+
+### 5. Deploy Edge Functions
+
+```bash
+# Deploy all functions
+supabase functions deploy
+
+# Or deploy individually
+supabase functions deploy upload-image
+supabase functions deploy process-selective-approval
+# ... etc
+```
+
+### 6. Set Supabase Secrets
+
+```bash
+# Via CLI
+supabase secrets set CLOUDFLARE_ACCOUNT_ID=[value]
+supabase secrets set CLOUDFLARE_IMAGES_API_TOKEN=[value]
+
+# Or via Dashboard
+# Project Settings → Secrets → Add Secret
+```
+
+### 7. Configure CloudFlare Images
+
+1. Create CloudFlare account
+2. Enable Images product
+3. Create API token with Images Write permission
+4. Get Account ID and Account Hash
+5. Configure variants (avatar, banner, card, etc.)
+
+### 8. Configure Novu (Optional)
+
+1. Create Novu Cloud account
+2. Create notification templates:
+ - content-approved
+ - content-rejected
+ - moderation-alert
+ - review-reply
+3. Get API key and Application Identifier
+4. Set environment variables
+
+### 9. Build Frontend
+
+```bash
+npm run build
+```
+
+### 10. Deploy Frontend
+
+**Option A: Lovable (Recommended)**
+- Deploy via Lovable interface
+- Automatic deployments on git push
+
+**Option B: Vercel**
+```bash
+npm install -g vercel
+vercel --prod
+```
+
+**Option C: Netlify**
+```bash
+npm install -g netlify-cli
+netlify deploy --prod
+```
+
+---
+
+## Post-Deployment Checklist
+
+### Verify Services
+
+- [ ] Frontend loads without errors
+- [ ] Can sign in with password
+- [ ] Can sign in with OAuth (Google, Discord)
+- [ ] Database queries work
+- [ ] Edge functions respond
+- [ ] CloudFlare image upload works
+- [ ] Notifications send (if enabled)
+
+### Create Admin User
+
+```sql
+-- In Supabase SQL Editor
+-- 1. Sign up user via UI first
+-- 2. Then grant superuser role
+INSERT INTO user_roles (user_id, role, granted_by)
+VALUES (
+ '[user-id-from-auth-users]',
+ 'superuser',
+ '[user-id-from-auth-users]' -- Self-grant for first superuser
+);
+```
+
+### Configure RLS
+
+Verify Row-Level Security is enabled:
+
+```sql
+-- Check RLS status
+SELECT tablename, rowsecurity
+FROM pg_tables
+WHERE schemaname = 'public';
+
+-- All should return rowsecurity = true
+```
+
+### Test Critical Flows
+
+1. Create test park submission
+2. Review in moderation queue
+3. Approve submission
+4. Verify park appears on site
+5. Check version created
+6. Test rollback
+
+---
+
+## Domain Configuration
+
+### Custom Domain (Lovable)
+
+1. Go to Project → Settings → Domains
+2. Add custom domain
+3. Update DNS records:
+ - CNAME: www → [lovable-subdomain]
+ - A: @ → [lovable-ip]
+4. Wait for SSL provisioning
+
+### Custom Domain (Vercel/Netlify)
+
+Follow platform-specific instructions for custom domains.
+
+---
+
+## Monitoring & Logging
+
+### Supabase Monitoring
+
+- **Dashboard**: Monitor database performance
+- **Logs**: View edge function logs
+- **Analytics**: Track API usage
+
+### Frontend Monitoring
+
+Consider adding:
+- **Sentry**: Error tracking
+- **PostHog**: Product analytics
+- **LogRocket**: Session replay
+
+### Database Monitoring
+
+```sql
+-- Active connections
+SELECT count(*) FROM pg_stat_activity;
+
+-- Slow queries
+SELECT * FROM pg_stat_statements
+WHERE mean_exec_time > 100
+ORDER BY mean_exec_time DESC
+LIMIT 10;
+
+-- Table sizes
+SELECT
+ tablename,
+ pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
+FROM pg_tables
+WHERE schemaname = 'public'
+ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
+```
+
+---
+
+## Backup & Recovery
+
+### Database Backups
+
+Supabase Pro automatically backs up:
+- Daily backups kept for 7 days
+- Point-in-time recovery
+
+Manual backup:
+
+```bash
+supabase db dump > backup.sql
+```
+
+### Image Backups
+
+CloudFlare Images are automatically backed up by CloudFlare.
+
+To export all image IDs:
+
+```sql
+COPY (
+ SELECT cloudflare_image_id, cloudflare_image_url
+ FROM (
+ SELECT banner_image_id AS cloudflare_image_id, banner_image_url AS cloudflare_image_url FROM parks
+ UNION
+ SELECT card_image_id, card_image_url FROM parks
+ UNION
+ SELECT banner_image_id, banner_image_url FROM rides
+ UNION
+ SELECT card_image_id, card_image_url FROM rides
+ -- ... etc
+ ) AS images
+ WHERE cloudflare_image_id IS NOT NULL
+) TO '/tmp/image_ids.csv' WITH CSV HEADER;
+```
+
+---
+
+## Scaling Considerations
+
+### Database Optimization
+
+**Indexes:**
+- Add indexes for frequently filtered columns
+- Add composite indexes for multi-column queries
+- Monitor slow query log
+
+**Caching:**
+- Use React Query caching aggressively
+- Cache static data (entity lists) for 5+ minutes
+- Cache dynamic data (moderation queue) for 10-30 seconds
+
+**Partitioning:**
+Consider partitioning large tables when:
+- `entity_page_views` > 10M rows
+- `notification_logs` > 5M rows
+
+### Edge Function Optimization
+
+- Keep functions small and focused
+- Use connection pooling for database
+- Cache frequently accessed data
+- Consider Deno KV for session data
+
+### Frontend Optimization
+
+**Code Splitting:**
+```typescript
+const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
+```
+
+**Image Optimization:**
+- Use CloudFlare variants (thumbnail, card, banner)
+- Lazy load images with `loading="lazy"`
+- Use AVIF/WebP formats
+
+**Bundle Size:**
+- Analyze bundle: `npm run build -- --analyze`
+- Target: < 1MB initial bundle
+- Lazy load admin pages
+
+---
+
+## Security Checklist
+
+- [ ] RLS enabled on all tables
+- [ ] Supabase secrets set (not in .env)
+- [ ] CloudFlare API token restricted to Images only
+- [ ] MFA required for admin actions
+- [ ] Rate limiting enabled on edge functions
+- [ ] CORS configured correctly
+- [ ] HTTPS enforced
+- [ ] Content Security Policy configured
+- [ ] XSS protection enabled
+
+---
+
+## Troubleshooting
+
+### Common Issues
+
+**Build fails:**
+```bash
+# Clear cache and rebuild
+rm -rf node_modules .next dist
+npm install
+npm run build
+```
+
+**Edge function 503:**
+- Check function logs in Supabase Dashboard
+- Verify secrets are set
+- Check function timeout (default 60s)
+
+**Image upload fails:**
+- Verify CloudFlare credentials
+- Check CORS configuration
+- Verify account not over quota
+
+**RLS blocking queries:**
+- Check if user authenticated
+- Verify policy conditions
+- Check AAL level (MFA)
+
+---
+
+## Maintenance
+
+### Regular Tasks
+
+**Weekly:**
+- [ ] Review error logs
+- [ ] Check slow query log
+- [ ] Monitor disk usage
+- [ ] Review stuck locks in moderation queue
+
+**Monthly:**
+- [ ] Update dependencies (`npm update`)
+- [ ] Review security advisories
+- [ ] Clean up old page views (90+ days)
+- [ ] Archive old notification logs
+
+**Quarterly:**
+- [ ] Review database performance
+- [ ] Optimize indexes
+- [ ] Review and update RLS policies
+- [ ] Audit user permissions
+
+---
+
+## Rollback Procedure
+
+If deployment fails:
+
+1. **Revert Frontend:**
+ ```bash
+ git revert HEAD
+ git push
+ # Or use platform's rollback feature
+ ```
+
+2. **Revert Database:**
+ ```bash
+ supabase db reset # Local only!
+ # Production: Restore from backup
+ ```
+
+3. **Revert Edge Functions:**
+ ```bash
+ supabase functions deploy [function-name] --version [previous-version]
+ ```
+
+---
+
+## Support
+
+For deployment issues:
+- Check documentation: [docs/](./docs/)
+- Review logs: Supabase Dashboard → Logs
+- Create issue: GitHub Issues
+- Contact: [support email]
+
+---
+
+**Last Updated:** 2025-01-20
diff --git a/docs/FRONTEND_ARCHITECTURE.md b/docs/FRONTEND_ARCHITECTURE.md
new file mode 100644
index 00000000..eb2adbb7
--- /dev/null
+++ b/docs/FRONTEND_ARCHITECTURE.md
@@ -0,0 +1,658 @@
+# Frontend Architecture
+
+Complete documentation of ThrillWiki's React frontend architecture, routing, state management, and UI patterns.
+
+---
+
+## Technology Stack
+
+- **Framework**: React 18.3 with TypeScript
+- **Build Tool**: Vite
+- **Styling**: Tailwind CSS with custom design system
+- **UI Components**: shadcn/ui (Radix UI primitives)
+- **Routing**: React Router v6
+- **Data Fetching**: TanStack Query (React Query)
+- **Forms**: React Hook Form with Zod validation
+- **State Machines**: Custom TypeScript state machines
+- **Icons**: lucide-react
+
+---
+
+## Routing Structure
+
+### URL Pattern Standards
+
+**Parks:**
+```
+/parks/ → Global park list
+/parks/{parkSlug}/ → Individual park detail
+/parks/{parkSlug}/rides/ → Park's ride list
+/operators/{operatorSlug}/parks/ → Operator's parks
+/owners/{ownerSlug}/parks/ → Owner's parks
+```
+
+**Rides:**
+```
+/rides/ → Global ride list
+/parks/{parkSlug}/rides/{rideSlug}/ → Ride detail (nested under park)
+/manufacturers/{manufacturerSlug}/rides/ → Manufacturer's rides
+/manufacturers/{manufacturerSlug}/models/ → Manufacturer's models
+/designers/{designerSlug}/rides/ → Designer's rides
+```
+
+**Admin:**
+```
+/admin/ → Admin dashboard
+/admin/moderation/ → Moderation queue
+/admin/reports/ → Reports queue
+/admin/users/ → User management
+/admin/system-log/ → System activity log
+/admin/blog/ → Blog management
+/admin/settings/ → Admin settings
+```
+
+### Route Configuration
+
+```typescript
+// src/App.tsx
+
+
+ {/* Public routes */}
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ {/* Admin routes - auth guard applied in page component */}
+ } />
+ } />
+
+ {/* Auth routes */}
+ } />
+ } />
+
+ {/* 404 */}
+ } />
+
+
+```
+
+---
+
+## Component Architecture
+
+### Component Organization
+
+```
+src/components/
+├── admin/ # Admin forms and management tools
+│ ├── ParkForm.tsx
+│ ├── RideForm.tsx
+│ ├── UserManagement.tsx
+│ └── ...
+├── auth/ # Authentication components
+│ ├── AuthModal.tsx
+│ ├── MFAChallenge.tsx
+│ ├── TOTPSetup.tsx
+│ └── ...
+├── moderation/ # Moderation queue components
+│ ├── ModerationQueue.tsx
+│ ├── SubmissionReviewManager.tsx
+│ ├── QueueFilters.tsx
+│ └── ...
+├── parks/ # Park display components
+│ ├── ParkCard.tsx
+│ ├── ParkGrid.tsx
+│ ├── ParkFilters.tsx
+│ └── ...
+├── rides/ # Ride display components
+├── upload/ # Image upload components
+├── versioning/ # Version history components
+├── layout/ # Layout components
+│ ├── Header.tsx
+│ ├── Footer.tsx
+│ └── AdminLayout.tsx
+├── common/ # Shared components
+│ ├── LoadingGate.tsx
+│ ├── ProfileBadge.tsx
+│ └── SortControls.tsx
+└── ui/ # shadcn/ui base components
+ ├── button.tsx
+ ├── dialog.tsx
+ └── ...
+```
+
+### Page Components
+
+Top-level route components that:
+- Handle auth guards (admin pages use `useAdminGuard`)
+- Fetch initial data with React Query
+- Implement layout structure
+- Pass data to feature components
+
+```typescript
+// Example: AdminModeration.tsx
+export function AdminModeration() {
+ const { loading } = useAdminGuard('moderator');
+
+ if (loading) return ;
+
+ return (
+
+
+
+
+
+ );
+}
+```
+
+### Feature Components
+
+Domain-specific components with business logic.
+
+```typescript
+// Example: ModerationQueue.tsx
+export function ModerationQueue() {
+ const {
+ items,
+ isLoading,
+ filters,
+ pagination,
+ handleAction,
+ } = useModerationQueueManager();
+
+ return (
+
+
+
+ {items.map(item => (
+
+ ))}
+
+
+ );
+}
+```
+
+---
+
+## State Management
+
+### React Query (TanStack Query)
+
+Used for ALL server state (data fetching, caching, mutations).
+
+**Configuration:**
+
+```typescript
+// App.tsx
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ retry: 1,
+ staleTime: 30000, // 30s fresh
+ gcTime: 5 * 60 * 1000, // 5min cache
+ },
+ },
+});
+
+
+
+
+```
+
+**Custom Hook Pattern:**
+
+```typescript
+// src/hooks/useEntityVersions.ts
+export function useEntityVersions(entityType: EntityType, entityId: string) {
+ const query = useQuery({
+ queryKey: ['versions', entityType, entityId],
+ queryFn: () => fetchVersions(entityType, entityId),
+ enabled: !!entityId,
+ });
+
+ const mutation = useMutation({
+ mutationFn: rollbackVersion,
+ onSuccess: () => {
+ queryClient.invalidateQueries(['versions', entityType, entityId]);
+ },
+ });
+
+ return {
+ versions: query.data,
+ isLoading: query.isLoading,
+ rollback: mutation.mutate
+ };
+}
+```
+
+### State Machines
+
+Used for complex workflows with strict transitions.
+
+**1. moderationStateMachine.ts** - Moderation workflow
+
+```typescript
+type ModerationState =
+ | { status: 'idle' }
+ | { status: 'claiming'; itemId: string }
+ | { status: 'locked'; itemId: string; lockExpires: string }
+ | { status: 'loading_data'; itemId: string; lockExpires: string }
+ | { status: 'reviewing'; itemId: string; lockExpires: string; reviewData: SubmissionItemWithDeps[] }
+ | { status: 'approving'; itemId: string }
+ | { status: 'rejecting'; itemId: string }
+ | { status: 'complete'; itemId: string; result: 'approved' | 'rejected' }
+ | { status: 'error'; itemId: string; error: string }
+ | { status: 'lock_expired'; itemId: string };
+
+type ModerationAction =
+ | { type: 'CLAIM_ITEM'; payload: { itemId: string } }
+ | { type: 'LOCK_ACQUIRED'; payload: { lockExpires: string } }
+ | { type: 'LOAD_DATA' }
+ | { type: 'DATA_LOADED'; payload: { reviewData: SubmissionItemWithDeps[] } }
+ | { type: 'START_APPROVAL' }
+ | { type: 'START_REJECTION' }
+ | { type: 'COMPLETE'; payload: { result: 'approved' | 'rejected' } }
+ | { type: 'ERROR'; payload: { error: string } }
+ | { type: 'LOCK_EXPIRED' }
+ | { type: 'RESET' };
+
+function moderationReducer(
+ state: ModerationState,
+ action: ModerationAction
+): ModerationState {
+ // ... transition logic with guards
+}
+
+// Usage in SubmissionReviewManager.tsx
+const [state, dispatch] = useReducer(moderationReducer, { status: 'idle' });
+```
+
+**2. deletionDialogMachine.ts** - Account deletion wizard
+
+```typescript
+type DeletionStep = 'warning' | 'confirm' | 'code';
+
+type DeletionDialogState = {
+ step: DeletionStep;
+ confirmationCode: string;
+ codeReceived: boolean;
+ loading: boolean;
+ error: string | null;
+};
+
+// Usage in AccountDeletionDialog.tsx
+```
+
+### React Hook Form
+
+Used for ALL forms with Zod validation.
+
+```typescript
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+
+const parkFormSchema = z.object({
+ name: z.string().min(1, 'Name required').max(255),
+ slug: z.string().regex(/^[a-z0-9-]+$/),
+ park_type: z.enum(['theme_park', 'amusement_park', 'water_park']),
+ opening_date: z.string().optional(),
+ // ... all fields validated
+});
+
+export function ParkForm() {
+ const form = useForm({
+ resolver: zodResolver(parkFormSchema),
+ defaultValues: initialData,
+ });
+
+ const onSubmit = async (data: ParkFormData) => {
+ await submitParkCreation(data, user.id);
+ // Goes to moderation queue, NOT direct DB write
+ };
+
+ return (
+
+
+ );
+}
+```
+
+### Context Providers
+
+**1. AuthProvider** - Global auth state
+
+```typescript
+// src/hooks/useAuth.tsx
+export function AuthProvider({ children }) {
+ const [user, setUser] = useState(null);
+ const [session, setSession] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ supabase.auth.getSession().then(({ data: { session } }) => {
+ setSession(session);
+ setUser(session?.user ?? null);
+ setLoading(false);
+ });
+
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(
+ (_event, session) => {
+ setSession(session);
+ setUser(session?.user ?? null);
+ }
+ );
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+**2. ThemeProvider** - Light/dark mode
+
+```typescript
+// src/components/theme/ThemeProvider.tsx
+export function ThemeProvider({ children }) {
+ const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+ root.classList.remove('light', 'dark');
+
+ if (theme === 'system') {
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light';
+ root.classList.add(systemTheme);
+ } else {
+ root.classList.add(theme);
+ }
+ }, [theme]);
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+**3. LocationAutoDetectProvider** - Auto-detect measurement system
+
+```typescript
+// src/components/providers/LocationAutoDetectProvider.tsx
+export function LocationAutoDetectProvider() {
+ useEffect(() => {
+ const detectLocation = async () => {
+ const { data } = await supabase.functions.invoke('detect-location');
+ if (data?.country) {
+ const system = getMeasurementSystemFromCountry(data.country);
+ // Update user preferences
+ }
+ };
+
+ detectLocation();
+ }, []);
+
+ return null;
+}
+```
+
+---
+
+## UI/UX Patterns
+
+### Design System
+
+- **Colors**: HSL-based semantic tokens in `index.css`
+- **Typography**: Custom font scale with Tailwind classes
+- **Spacing**: Consistent spacing scale (4px, 8px, 16px, etc.)
+- **Components**: shadcn/ui with custom variants
+- **Responsive**: Mobile-first, breakpoints at sm, md, lg, xl
+
+**Color Tokens (index.css):**
+
+```css
+:root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --muted: 210 40% 96.1%;
+ --accent: 210 40% 96.1%;
+ --destructive: 0 84.2% 60.2%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+ --radius: 0.5rem;
+}
+
+.dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ /* ... dark mode tokens */
+}
+```
+
+### Reusable UI Components
+
+**AdminPageLayout** - Wraps admin pages
+
+```typescript
+
+ {children}
+
+```
+
+**LoadingGate** - Handles loading/error states
+
+```typescript
+
+
+
+```
+
+**ProfileBadge** - User display with role badges
+
+```typescript
+
+```
+
+---
+
+## Form Patterns
+
+### Entity Forms
+
+All entity forms follow this structure:
+
+1. **Schema Definition** (Zod)
+2. **Form Setup** (React Hook Form)
+3. **Image Upload** (EntityMultiImageUploader)
+4. **Submit Handler** (entitySubmissionHelpers)
+
+```typescript
+// 1. Schema
+const parkFormSchema = z.object({
+ name: z.string().min(1).max(255),
+ slug: z.string().regex(/^[a-z0-9-]+$/),
+ // ... all fields
+});
+
+// 2. Form
+const form = useForm({
+ resolver: zodResolver(parkFormSchema),
+ defaultValues: initialData || defaultValues,
+});
+
+// 3. Submit
+const onSubmit = async (data: ParkFormData) => {
+ if (isEditing) {
+ await submitParkUpdate(parkId, data, user.id);
+ } else {
+ await submitParkCreation(data, user.id);
+ }
+ toast({ title: "Submitted for review" });
+};
+```
+
+### Image Upload
+
+```typescript
+const [imageAssignments, setImageAssignments] = useState({
+ banner: null,
+ card: null,
+ uploaded: []
+});
+
+
+```
+
+---
+
+## Custom Hooks
+
+### Data Fetching Hooks
+
+```typescript
+// src/hooks/useEntityVersions.ts
+export function useEntityVersions(entityType, entityId)
+
+// src/hooks/useModerationQueue.ts
+export function useModerationQueue(filters, pagination)
+
+// src/hooks/useProfile.tsx
+export function useProfile(userId)
+
+// src/hooks/useUserRole.ts
+export function useUserRole()
+```
+
+### Utility Hooks
+
+```typescript
+// src/hooks/useDebounce.ts
+export function useDebounce(value, delay)
+
+// src/hooks/useMobile.tsx
+export function useMobile()
+
+// src/hooks/useUnitPreferences.ts
+export function useUnitPreferences()
+```
+
+### Guard Hooks
+
+```typescript
+// src/hooks/useAdminGuard.ts
+export function useAdminGuard(requiredRole: AppRole = 'moderator')
+
+// src/hooks/useRequireMFA.ts
+export function useRequireMFA()
+```
+
+---
+
+## Performance Optimizations
+
+### React Query Caching
+
+```typescript
+// Aggressive caching for static data
+const { data: parks } = useQuery({
+ queryKey: ['parks'],
+ queryFn: fetchParks,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ gcTime: 10 * 60 * 1000, // 10 minutes
+});
+
+// Shorter caching for dynamic data
+const { data: queue } = useQuery({
+ queryKey: ['moderation', 'queue', filters],
+ queryFn: fetchQueue,
+ staleTime: 10000, // 10 seconds
+ gcTime: 60000, // 1 minute
+ refetchInterval: 30000, // Auto-refresh every 30s
+});
+```
+
+### Code Splitting
+
+```typescript
+// Lazy load admin pages
+const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
+const AdminModeration = lazy(() => import('./pages/AdminModeration'));
+
+}>
+ } />
+
+```
+
+### Memoization
+
+```typescript
+// Expensive computations
+const filteredItems = useMemo(() => {
+ return items.filter(item =>
+ item.status === statusFilter &&
+ item.entity_type === typeFilter
+ );
+}, [items, statusFilter, typeFilter]);
+
+// Callback stability
+const handleAction = useCallback((itemId: string, action: string) => {
+ performAction(itemId, action);
+}, [performAction]);
+```
+
+---
+
+**See Also:**
+- [DATABASE_ARCHITECTURE.md](./DATABASE_ARCHITECTURE.md) - Database schema
+- [SUBMISSION_FLOW.md](./SUBMISSION_FLOW.md) - Submission workflow
+- [AUTHENTICATION.md](./AUTHENTICATION.md) - Auth implementation
diff --git a/docs/TESTING_GUIDE.md b/docs/TESTING_GUIDE.md
new file mode 100644
index 00000000..36b85d34
--- /dev/null
+++ b/docs/TESTING_GUIDE.md
@@ -0,0 +1,501 @@
+# Testing Guide
+
+Comprehensive testing procedures and checklists for ThrillWiki.
+
+---
+
+## Testing Philosophy
+
+1. **Manual testing first** - Critical user flows tested manually
+2. **Database validation** - Verify data integrity with SQL queries
+3. **Error monitoring** - Watch logs during testing
+4. **Real-world scenarios** - Test with realistic data
+5. **Edge cases** - Test boundary conditions and error states
+
+---
+
+## Manual Testing Checklist
+
+### Authentication Flow
+
+**Password Sign In:**
+- [ ] Sign in with valid credentials
+- [ ] Sign in with invalid password
+- [ ] Sign in with non-existent email
+- [ ] Session persists after page refresh
+- [ ] Sign out works correctly
+
+**OAuth Sign In:**
+- [ ] Google OAuth flow completes
+- [ ] Discord OAuth flow completes
+- [ ] Profile created after first OAuth sign-in
+- [ ] OAuth profile data synced correctly
+
+**MFA Enrollment:**
+- [ ] Enroll TOTP factor successfully
+- [ ] QR code displays correctly
+- [ ] Backup codes generated
+- [ ] Verify code works
+- [ ] Invalid code rejected
+
+**MFA Challenge:**
+- [ ] MFA-enrolled user prompted for code on sign in
+- [ ] Valid code accepted, session upgraded to AAL2
+- [ ] Invalid code rejected
+- [ ] Step-up required for admin actions
+- [ ] Lock warning shows when AAL2 about to expire
+
+### Submission Flow
+
+**Park Creation:**
+- [ ] Form validation works (required fields, slug format)
+- [ ] Image upload works (banner, card, gallery)
+- [ ] Date precision selector works (day, month, year)
+- [ ] Location search and selection works
+- [ ] Operator/property owner selection works
+- [ ] Submission created in moderation queue
+- [ ] Toast confirmation shows
+- [ ] No direct write to parks table
+
+**Park Editing:**
+- [ ] Existing park loads in form
+- [ ] Change detection works (only changed fields submitted)
+- [ ] Image replacement works
+- [ ] Submission created with original_data
+- [ ] Edit appears in moderation queue
+
+**Ride Creation:**
+- [ ] All ride categories work
+- [ ] Coaster stats editor works (metric only)
+- [ ] Technical specs editor works
+- [ ] Former names editor works
+- [ ] Unit conversion helper displays
+- [ ] Submission created successfully
+
+**Company Creation:**
+- [ ] Company type selection works
+- [ ] Person type selection works
+- [ ] Founded date with precision works
+- [ ] Logo upload works
+- [ ] Submission created successfully
+
+**Ride Model Creation:**
+- [ ] Manufacturer required validation works
+- [ ] Category selection works
+- [ ] Submission created successfully
+
+### Moderation Queue
+
+**Queue Display:**
+- [ ] All pending submissions show
+- [ ] Filters work (entity type, status, search)
+- [ ] Sort options work
+- [ ] Pagination works
+- [ ] Stats display correctly
+
+**Claiming:**
+- [ ] Claim submission button works
+- [ ] Lock acquired (15 minutes)
+- [ ] Lock status displays
+- [ ] Another moderator cannot claim
+- [ ] Lock expires after 15 minutes
+
+**Lock Management:**
+- [ ] Lock timer displays correctly
+- [ ] Warning shows at 2 minutes remaining
+- [ ] Extend lock button works
+- [ ] Lock auto-expires if not extended
+- [ ] Expired lock shows error state
+
+**Review:**
+- [ ] Submission items display correctly
+- [ ] Change comparison shows (for edits)
+- [ ] Dependency visualization works
+- [ ] Individual item approval works
+- [ ] Individual item rejection works
+
+**Approval:**
+- [ ] Approve all works
+- [ ] Selective approval works
+- [ ] Edge function called successfully
+- [ ] Entity created/updated in database
+- [ ] Version created with correct attribution
+- [ ] Submission status updated to 'approved'
+- [ ] Lock released
+- [ ] Notification sent to submitter
+
+**Rejection:**
+- [ ] Rejection reason required
+- [ ] Reject all works
+- [ ] Submission status updated to 'rejected'
+- [ ] Lock released
+- [ ] Notification sent to submitter
+
+### Versioning System
+
+**Version Creation:**
+- [ ] Version created on entity INSERT
+- [ ] Version created on entity UPDATE
+- [ ] Version number increments
+- [ ] is_current flag set correctly
+- [ ] created_by set from session variable
+- [ ] submission_id linked correctly
+
+**Version History:**
+- [ ] All versions display in timeline
+- [ ] Version details show correctly
+- [ ] Created by user shows
+- [ ] Change type badge displays
+- [ ] Current version marked
+
+**Version Comparison:**
+- [ ] Select two versions
+- [ ] Compare button enabled
+- [ ] Diff shows changed fields only
+- [ ] Old vs new values display
+- [ ] Foreign key changes resolved to names
+
+**Rollback:**
+- [ ] Rollback button shows (not for current version)
+- [ ] Rollback creates new submission
+- [ ] Submission goes to moderation queue
+- [ ] On approval, new version created
+
+### Image Upload
+
+**Upload Flow:**
+- [ ] Drag & drop works
+- [ ] File picker works
+- [ ] Multiple files upload
+- [ ] Progress indicator shows
+- [ ] CloudFlare upload succeeds
+- [ ] Image preview displays
+- [ ] Caption editor works
+- [ ] Set as banner works
+- [ ] Set as card works
+- [ ] Remove image works
+- [ ] Blob URLs cleaned up
+
+**Error Handling:**
+- [ ] File too large rejected
+- [ ] Invalid file type rejected
+- [ ] Network error handled
+- [ ] Partial upload failure handled
+- [ ] Cleanup on error works
+
+### Unit System
+
+**Display Conversion:**
+- [ ] Metric user sees km/h, m, cm
+- [ ] Imperial user sees mph, ft, in
+- [ ] Auto-detection sets correct system
+- [ ] Manual preference change works
+- [ ] Conversion accurate
+
+**Input Validation:**
+- [ ] Coaster stats only accept metric
+- [ ] Technical specs enforce metric
+- [ ] Conversion helper shows correct formula
+- [ ] Invalid unit rejected
+
+### Date Handling
+
+**Date Input:**
+- [ ] Date picker works (day precision)
+- [ ] Month picker works (month precision)
+- [ ] Year input works (year precision)
+- [ ] Precision selector works
+- [ ] Date stored as YYYY-MM-DD
+
+**Date Display:**
+- [ ] Day precision: "July 15, 2024"
+- [ ] Month precision: "July 2024"
+- [ ] Year precision: "2024"
+- [ ] Empty date handled gracefully
+
+### Notifications
+
+**Subscriber Creation:**
+- [ ] Novu subscriber created on sign up
+- [ ] Subscriber ID stored in database
+- [ ] Profile data synced to Novu
+
+**Notification Triggering:**
+- [ ] Submission approved notification sent
+- [ ] Submission rejected notification sent
+- [ ] Review reply notification sent
+- [ ] Moderation alert sent to moderators
+
+**Notification Center:**
+- [ ] In-app center displays
+- [ ] Unread count shows
+- [ ] Mark as read works
+- [ ] Notification click navigates correctly
+
+### User Roles & Permissions
+
+**Role Assignment:**
+- [ ] Admin can grant moderator role
+- [ ] Admin can grant admin role
+- [ ] Only superuser can grant superuser role
+- [ ] Role revocation works
+- [ ] Audit log entry created
+
+**Permission Checks:**
+- [ ] User cannot access admin pages
+- [ ] Moderator can access moderation queue
+- [ ] Admin can access user management
+- [ ] Superuser can access all admin features
+- [ ] MFA required for sensitive actions
+
+---
+
+## Database Validation Queries
+
+### Check Submission Flow
+
+```sql
+-- Verify no direct writes to entity tables (should be empty if moderation works)
+SELECT * FROM parks
+WHERE created_at > NOW() - INTERVAL '1 hour'
+ AND id NOT IN (
+ SELECT (item_data->>'park_id')::UUID
+ FROM submission_items
+ WHERE item_type = 'park' AND status = 'approved'
+ );
+```
+
+### Check Versioning
+
+```sql
+-- Verify version created for recent park update
+SELECT
+ p.name,
+ pv.version_number,
+ pv.change_type,
+ pv.created_by,
+ pv.submission_id,
+ pv.is_current
+FROM parks p
+JOIN park_versions pv ON pv.park_id = p.id
+WHERE p.updated_at > NOW() - INTERVAL '1 hour'
+ORDER BY p.id, pv.version_number DESC;
+```
+
+### Check Lock Status
+
+```sql
+-- Identify stuck locks
+SELECT
+ id,
+ submission_type,
+ assigned_to,
+ assigned_at,
+ locked_until,
+ status
+FROM content_submissions
+WHERE status = 'pending'
+ AND locked_until IS NOT NULL
+ AND locked_until < NOW();
+```
+
+### Check RLS Policies
+
+```sql
+-- Test entity table RLS (should fail as non-service role)
+BEGIN;
+SET LOCAL ROLE authenticated;
+INSERT INTO parks (name, slug, park_type)
+VALUES ('Test Park', 'test-park', 'theme_park');
+-- Should fail with permission denied
+ROLLBACK;
+```
+
+---
+
+## Error Monitoring
+
+### Console Logs
+
+Watch browser console for:
+- [ ] No React errors
+- [ ] No TypeScript errors
+- [ ] No 401 Unauthorized errors
+- [ ] No 403 Forbidden errors
+- [ ] No 500 Server errors
+
+### Network Tab
+
+Monitor API calls:
+- [ ] All Supabase requests succeed
+- [ ] Edge function calls return 200
+- [ ] CloudFlare uploads succeed
+- [ ] Realtime subscriptions connected
+- [ ] No excessive requests (n+1 queries)
+
+### Edge Function Logs
+
+Check Supabase logs for:
+- [ ] No unhandled exceptions
+- [ ] Request/response logged
+- [ ] Duration reasonable (<5s)
+- [ ] No infinite loops
+
+---
+
+## Performance Testing
+
+### Page Load Times
+
+- [ ] Homepage loads < 2s
+- [ ] Park detail loads < 3s
+- [ ] Moderation queue loads < 3s
+- [ ] Image heavy pages load < 5s
+
+### Query Performance
+
+```sql
+-- Slow query detection
+SELECT
+ query,
+ calls,
+ total_time,
+ mean_time,
+ max_time
+FROM pg_stat_statements
+WHERE mean_time > 100 -- Over 100ms average
+ORDER BY mean_time DESC
+LIMIT 20;
+```
+
+### Bundle Size
+
+```bash
+npm run build
+# Check dist/ folder size
+# Target: < 1MB initial bundle
+```
+
+---
+
+## User Acceptance Testing
+
+### New User Journey
+
+1. [ ] Sign up with email/password
+2. [ ] Verify email (if required)
+3. [ ] Complete profile setup
+4. [ ] Browse parks
+5. [ ] Add ride credit
+6. [ ] Write review
+7. [ ] Submit new park (goes to moderation)
+
+### Moderator Journey
+
+1. [ ] Sign in with moderator account
+2. [ ] View moderation queue
+3. [ ] Claim submission
+4. [ ] Review submission details
+5. [ ] Approve submission
+6. [ ] Verify entity live on site
+7. [ ] Check version history
+
+### Admin Journey
+
+1. [ ] Sign in with admin account
+2. [ ] Complete MFA challenge
+3. [ ] View user management
+4. [ ] Grant moderator role
+5. [ ] View system activity log
+6. [ ] Check admin settings
+
+---
+
+## Regression Testing
+
+After major changes, re-test:
+
+**Critical Paths:**
+- [ ] Auth flow (sign in, sign up, sign out)
+- [ ] Submission flow (create park, moderation, approval)
+- [ ] Version system (create, compare, rollback)
+- [ ] Image upload (upload, set variants, delete)
+
+**Secondary Paths:**
+- [ ] Search functionality
+- [ ] Filters and sorting
+- [ ] User profiles
+- [ ] Reviews and ratings
+- [ ] Lists management
+
+---
+
+## Test Data Generation
+
+Use the built-in test data generator:
+
+```typescript
+// Navigate to /admin/settings
+// Click "Test Data Generator" tab
+// Select entities to generate
+// Click "Generate Test Data"
+```
+
+Or use the edge function:
+
+```bash
+curl -X POST https://[project-ref].supabase.co/functions/v1/seed-test-data \
+ -H "Authorization: Bearer [anon-key]" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "parks": 10,
+ "rides": 50,
+ "companies": 5,
+ "users": 20
+ }'
+```
+
+---
+
+## Bug Reporting Template
+
+```markdown
+**Bug Description:**
+Clear description of the issue
+
+**Steps to Reproduce:**
+1. Go to...
+2. Click on...
+3. Enter...
+4. Observe...
+
+**Expected Behavior:**
+What should happen
+
+**Actual Behavior:**
+What actually happens
+
+**Environment:**
+- Browser: Chrome 120
+- OS: macOS 14
+- User Role: Moderator
+- Device: Desktop
+
+**Console Errors:**
+```
+[Paste console errors]
+```
+
+**Screenshots:**
+[Attach screenshots if relevant]
+
+**Database State:**
+[Relevant database records if applicable]
+```
+
+---
+
+**See Also:**
+- [PHASE_5_TESTING.md](./PHASE_5_TESTING.md) - Original testing plan
+- [TEST_DATA_GENERATOR.md](./TEST_DATA_GENERATOR.md) - Test data generation
+- [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) - Common issues and solutions
diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md
new file mode 100644
index 00000000..b21f31ab
--- /dev/null
+++ b/docs/TROUBLESHOOTING.md
@@ -0,0 +1,614 @@
+# Troubleshooting Guide
+
+Common issues and their solutions for ThrillWiki.
+
+---
+
+## Authentication Issues
+
+### Cannot Sign In
+
+**Symptom:** Sign in button does nothing or shows error
+
+**Possible Causes:**
+1. Invalid credentials
+2. Email not verified
+3. Account banned/deactivated
+4. Supabase connection error
+
+**Solutions:**
+```typescript
+// Check browser console for errors
+// Look for auth errors in console
+
+// Verify Supabase connection
+const { data, error } = await supabase.auth.getSession();
+console.log('Session:', data, error);
+
+// Check if email verified (if required)
+SELECT email, email_confirmed_at FROM auth.users WHERE email = '[user-email]';
+```
+
+### MFA Not Working
+
+**Symptom:** MFA code not accepted or not prompted
+
+**Possible Causes:**
+1. Time sync issue (TOTP codes are time-based)
+2. Wrong factor ID
+3. Challenge not created
+4. AAL level not checked
+
+**Solutions:**
+```typescript
+// Verify device time is correct (CRITICAL for TOTP)
+// Check enrolled factors
+const { data } = await supabase.auth.mfa.listFactors();
+console.log('Enrolled factors:', data);
+
+// Create challenge manually
+const factor = data.totp[0];
+const { data: challenge } = await supabase.auth.mfa.challenge({
+ factorId: factor.id
+});
+
+// Verify code
+const { error } = await supabase.auth.mfa.verify({
+ factorId: factor.id,
+ challengeId: challenge.id,
+ code: '[6-digit-code]'
+});
+```
+
+### Session Expired
+
+**Symptom:** User logged out unexpectedly
+
+**Possible Causes:**
+1. Session timeout (default 1 hour)
+2. Token refresh failed
+3. Multiple tabs/windows
+4. AAL2 expired (MFA users)
+
+**Solutions:**
+```typescript
+// Check session status
+const { data: { session } } = await supabase.auth.getSession();
+if (!session) {
+ // Redirect to login
+}
+
+// Refresh session manually
+const { data, error } = await supabase.auth.refreshSession();
+
+// Monitor auth state changes
+supabase.auth.onAuthStateChange((event, session) => {
+ console.log('Auth event:', event, session);
+});
+```
+
+---
+
+## Submission Issues
+
+### Submission Not Appearing in Queue
+
+**Symptom:** Created submission doesn't show in moderation queue
+
+**Possible Causes:**
+1. Database write failed
+2. Status filter hiding submission
+3. RLS blocking view
+4. Realtime subscription not connected
+
+**Solutions:**
+```sql
+-- Check if submission created
+SELECT * FROM content_submissions
+WHERE user_id = '[user-id]'
+ORDER BY created_at DESC LIMIT 5;
+
+-- Check submission items
+SELECT * FROM submission_items
+WHERE submission_id = '[submission-id]';
+
+-- Check RLS policies
+SET LOCAL ROLE authenticated;
+SET LOCAL "request.jwt.claims" = '{"sub": "[moderator-user-id]"}';
+SELECT * FROM content_submissions WHERE status = 'pending';
+```
+
+### Cannot Approve Submission
+
+**Symptom:** Approve button disabled or approval fails
+
+**Possible Causes:**
+1. Lock expired
+2. No active lock
+3. Dependencies not approved
+4. Edge function error
+
+**Solutions:**
+```typescript
+// Check lock status
+const { data: submission } = await supabase
+ .from('content_submissions')
+ .select('*, locked_until, assigned_to')
+ .eq('id', submissionId)
+ .single();
+
+if (new Date(submission.locked_until) < new Date()) {
+ console.error('Lock expired');
+}
+
+// Check dependencies
+const { data: items } = await supabase
+ .from('submission_items')
+ .select('*, depends_on')
+ .eq('submission_id', submissionId);
+
+const blocked = items.filter(item =>
+ item.depends_on &&
+ items.find(dep => dep.id === item.depends_on && dep.status !== 'approved')
+);
+console.log('Blocked items:', blocked);
+```
+
+### Image Upload Fails
+
+**Symptom:** Image upload error or stuck at uploading
+
+**Possible Causes:**
+1. File too large (>10MB)
+2. Invalid file type
+3. CloudFlare API error
+4. Network error
+5. CORS issue
+
+**Solutions:**
+```typescript
+// Check file size and type
+const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
+const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
+
+if (file.size > MAX_FILE_SIZE) {
+ throw new Error('File too large');
+}
+if (!ALLOWED_TYPES.includes(file.type)) {
+ throw new Error('Invalid file type');
+}
+
+// Check CloudFlare credentials
+console.log('CF Account Hash:', import.meta.env.VITE_CLOUDFLARE_ACCOUNT_HASH);
+
+// Test edge function
+const { data, error } = await supabase.functions.invoke('upload-image', {
+ body: { action: 'get-upload-url' }
+});
+console.log('Upload URL response:', data, error);
+
+// Check network tab for CORS errors
+```
+
+---
+
+## Moderation Queue Issues
+
+### Lock Stuck
+
+**Symptom:** Submission locked but moderator not actively reviewing
+
+**Possible Causes:**
+1. Moderator closed browser without releasing lock
+2. Lock extension failed
+3. Database lock not expired
+
+**Solutions:**
+```sql
+-- Find stuck locks
+SELECT
+ id,
+ submission_type,
+ assigned_to,
+ assigned_at,
+ locked_until,
+ EXTRACT(EPOCH FROM (NOW() - locked_until))/60 AS minutes_over
+FROM content_submissions
+WHERE locked_until IS NOT NULL
+ AND locked_until < NOW()
+ AND status = 'pending';
+
+-- Release stuck lock (as admin)
+UPDATE content_submissions
+SET assigned_to = NULL,
+ assigned_at = NULL,
+ locked_until = NULL
+WHERE id = '[submission-id]';
+```
+
+### Realtime Not Working
+
+**Symptom:** Queue doesn't update with new submissions
+
+**Possible Causes:**
+1. Realtime subscription not connected
+2. RLS blocking realtime
+3. Filter mismatch
+4. Subscription channel wrong
+
+**Solutions:**
+```typescript
+// Check subscription status
+const subscription = supabase
+ .channel('moderation_queue')
+ .on('postgres_changes', {
+ event: '*',
+ schema: 'public',
+ table: 'content_submissions',
+ filter: 'status=eq.pending'
+ }, payload => {
+ console.log('Realtime update:', payload);
+ })
+ .subscribe((status) => {
+ console.log('Subscription status:', status);
+ });
+
+// Verify RLS allows realtime
+SELECT * FROM content_submissions WHERE status = 'pending';
+-- If this works, realtime should too
+```
+
+---
+
+## Versioning Issues
+
+### Version Not Created
+
+**Symptom:** Entity updated but no version record
+
+**Possible Causes:**
+1. Trigger not firing
+2. Session variables not set
+3. Trigger conditions not met
+4. Database error
+
+**Solutions:**
+```sql
+-- Check if trigger exists
+SELECT tgname, tgenabled
+FROM pg_trigger
+WHERE tgrelid = 'parks'::regclass;
+
+-- Check session variables during update
+SELECT
+ current_setting('app.current_user_id', true) AS user_id,
+ current_setting('app.submission_id', true) AS submission_id;
+
+-- Manual version creation (as admin)
+INSERT INTO park_versions (
+ park_id, version_number, created_by, change_type,
+ name, slug, description, -- ... all fields
+)
+SELECT
+ id,
+ COALESCE((SELECT MAX(version_number) FROM park_versions WHERE park_id = p.id), 0) + 1,
+ '[user-id]',
+ 'updated',
+ name, slug, description -- ... all fields
+FROM parks p
+WHERE id = '[park-id]';
+```
+
+### Version Comparison Fails
+
+**Symptom:** Cannot compare versions or diff empty
+
+**Possible Causes:**
+1. Version IDs invalid
+2. Function error
+3. No changes between versions
+
+**Solutions:**
+```sql
+-- Verify versions exist
+SELECT version_id, version_number, change_type
+FROM park_versions
+WHERE park_id = '[park-id]'
+ORDER BY version_number DESC;
+
+-- Test diff function
+SELECT get_version_diff(
+ 'park',
+ '[from-version-id]'::UUID,
+ '[to-version-id]'::UUID
+);
+
+-- If null, no changes detected
+-- Check if fields actually different
+SELECT
+ (SELECT name FROM park_versions WHERE version_id = '[from]') AS old_name,
+ (SELECT name FROM park_versions WHERE version_id = '[to]') AS new_name;
+```
+
+---
+
+## Performance Issues
+
+### Slow Queries
+
+**Symptom:** Pages load slowly, timeouts
+
+**Possible Causes:**
+1. Missing indexes
+2. N+1 queries
+3. Large dataset
+4. Complex joins
+
+**Solutions:**
+```sql
+-- Find slow queries
+SELECT
+ query,
+ calls,
+ total_exec_time,
+ mean_exec_time,
+ max_exec_time
+FROM pg_stat_statements
+WHERE mean_exec_time > 100
+ORDER BY mean_exec_time DESC
+LIMIT 20;
+
+-- Add indexes
+CREATE INDEX idx_rides_park_status ON rides(park_id, status);
+CREATE INDEX idx_submissions_status_type ON content_submissions(status, submission_type);
+
+-- Analyze query plan
+EXPLAIN ANALYZE
+SELECT * FROM rides WHERE park_id = '[park-id]' AND status = 'operating';
+```
+
+### React Query Stale Data
+
+**Symptom:** UI shows old data after update
+
+**Possible Causes:**
+1. Cache not invalidated
+2. Stale time too long
+3. Background refetch disabled
+
+**Solutions:**
+```typescript
+// Invalidate specific queries after mutation
+const mutation = useMutation({
+ mutationFn: updatePark,
+ onSuccess: () => {
+ queryClient.invalidateQueries(['parks', parkId]);
+ queryClient.invalidateQueries(['parks']); // List
+ }
+});
+
+// Force refetch
+await queryClient.refetchQueries(['parks']);
+
+// Disable cache for testing
+const { data } = useQuery({
+ queryKey: ['parks'],
+ queryFn: fetchParks,
+ staleTime: 0,
+ gcTime: 0,
+});
+```
+
+---
+
+## Build/Deploy Issues
+
+### Build Fails
+
+**Symptom:** `npm run build` errors
+
+**Possible Causes:**
+1. TypeScript errors
+2. Missing dependencies
+3. Environment variables missing
+4. Out of memory
+
+**Solutions:**
+```bash
+# Type check first
+npm run typecheck
+
+# Clear cache
+rm -rf node_modules dist .next
+npm install
+
+# Build with verbose logs
+npm run build -- --verbose
+
+# Increase memory limit
+NODE_OPTIONS=--max_old_space_size=4096 npm run build
+```
+
+### Edge Function Fails
+
+**Symptom:** 500 error from edge function
+
+**Possible Causes:**
+1. Secrets not set
+2. Syntax error
+3. Timeout
+4. Memory limit
+
+**Solutions:**
+```bash
+# Test locally
+supabase functions serve upload-image
+
+# Check logs
+supabase functions logs upload-image
+
+# Verify secrets
+supabase secrets list
+
+# Set secrets
+supabase secrets set CLOUDFLARE_API_TOKEN=[value]
+```
+
+---
+
+## Database Issues
+
+### RLS Blocking Query
+
+**Symptom:** Query returns empty when data exists
+
+**Possible Causes:**
+1. User not authenticated
+2. Wrong user role
+3. Policy conditions not met
+4. AAL level insufficient
+
+**Solutions:**
+```sql
+-- Test as authenticated user
+SET LOCAL ROLE authenticated;
+SET LOCAL "request.jwt.claims" = '{"sub": "[user-id]", "role": "authenticated"}';
+
+-- Check policies
+SELECT * FROM pg_policies WHERE tablename = 'content_submissions';
+
+-- Disable RLS temporarily (DEVELOPMENT ONLY)
+ALTER TABLE content_submissions DISABLE ROW LEVEL SECURITY;
+-- Remember to re-enable!
+```
+
+### Migration Fails
+
+**Symptom:** `supabase db push` fails
+
+**Possible Causes:**
+1. Conflicting constraints
+2. Data violates new schema
+3. Circular dependencies
+4. Permission issues
+
+**Solutions:**
+```sql
+-- Check for constraint violations
+SELECT * FROM parks WHERE name IS NULL OR name = '';
+
+-- Fix data before migration
+UPDATE parks SET name = 'Unnamed Park' WHERE name IS NULL;
+
+-- Drop constraints temporarily
+ALTER TABLE parks DROP CONSTRAINT IF EXISTS parks_name_check;
+
+-- Recreate after data fixed
+ALTER TABLE parks ADD CONSTRAINT parks_name_check CHECK (name IS NOT NULL AND name != '');
+```
+
+---
+
+## User Reports
+
+### "I can't submit a park"
+
+**Checklist:**
+1. Is user authenticated?
+2. Is user banned?
+3. Are all required fields filled?
+4. Is form validation passing?
+5. Check browser console for errors
+
+### "My submission disappeared"
+
+**Checklist:**
+1. Check submission status in database
+2. Was it approved/rejected?
+3. Check notification logs
+4. Verify submission wasn't deleted
+
+### "I can't see the moderation queue"
+
+**Checklist:**
+1. Is user a moderator?
+2. Is MFA completed (if enrolled)?
+3. Check RLS policies
+4. Verify user_roles table
+
+---
+
+## Emergency Procedures
+
+### Database Locked Up
+
+```sql
+-- Find blocking queries
+SELECT
+ blocked_locks.pid AS blocked_pid,
+ blocked_activity.usename AS blocked_user,
+ blocking_locks.pid AS blocking_pid,
+ blocking_activity.usename AS blocking_user,
+ blocked_activity.query AS blocked_query,
+ blocking_activity.query AS blocking_query
+FROM pg_catalog.pg_locks blocked_locks
+JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
+JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
+JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
+WHERE NOT blocked_locks.granted;
+
+-- Kill blocking query (CAREFUL!)
+SELECT pg_terminate_backend([blocking_pid]);
+```
+
+### Edge Function Infinite Loop
+
+```bash
+# Stop all executions
+supabase functions delete [function-name]
+
+# Fix and redeploy
+supabase functions deploy [function-name]
+```
+
+### Out of Database Connections
+
+```sql
+-- Check active connections
+SELECT count(*) FROM pg_stat_activity;
+
+-- Kill idle connections
+SELECT pg_terminate_backend(pid)
+FROM pg_stat_activity
+WHERE state = 'idle'
+ AND state_change < NOW() - INTERVAL '5 minutes';
+```
+
+---
+
+## Getting Help
+
+If stuck:
+
+1. **Check Documentation**
+ - [docs/](./docs/) folder
+ - [API documentation](./versioning/API.md)
+
+2. **Search Issues**
+ - GitHub Issues
+ - Supabase Discord
+ - Stack Overflow
+
+3. **Ask for Help**
+ - Create detailed issue
+ - Include error messages
+ - Provide reproduction steps
+ - Share relevant code/logs
+
+4. **Contact Support**
+ - For critical production issues
+ - Email: [support email]
+
+---
+
+**Last Updated:** 2025-01-20