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 ( +
+ + {/* form fields */} +
+ + ); +} +``` + +### 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