Add documentation to files

This commit is contained in:
gpt-engineer-app[bot]
2025-10-21 14:41:42 +00:00
parent e247beeb1e
commit bcba0a4f0c
6 changed files with 3022 additions and 0 deletions

View File

@@ -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

View File

@@ -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

446
docs/DEPLOYMENT.md Normal file
View File

@@ -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

View File

@@ -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
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/" element={<Index />} />
<Route path="/parks" element={<Parks />} />
<Route path="/parks/:slug" element={<ParkDetail />} />
<Route path="/parks/:parkSlug/rides" element={<ParkRides />} />
<Route path="/parks/:parkSlug/rides/:rideSlug" element={<RideDetail />} />
{/* Admin routes - auth guard applied in page component */}
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/admin/moderation" element={<AdminModeration />} />
{/* Auth routes */}
<Route path="/auth" element={<Auth />} />
<Route path="/auth/callback" element={<AuthCallback />} />
{/* 404 */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
```
---
## 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 <LoadingGate isLoading />;
return (
<AdminLayout>
<AdminPageLayout
title="Moderation Queue"
description="Review and approve submissions"
>
<ModerationQueue />
</AdminPageLayout>
</AdminLayout>
);
}
```
### Feature Components
Domain-specific components with business logic.
```typescript
// Example: ModerationQueue.tsx
export function ModerationQueue() {
const {
items,
isLoading,
filters,
pagination,
handleAction,
} = useModerationQueueManager();
return (
<div>
<QueueFilters {...filters} />
<QueueStats items={items} />
{items.map(item => (
<QueueItem
key={item.id}
item={item}
onAction={handleAction}
/>
))}
<QueuePagination {...pagination} />
</div>
);
}
```
---
## 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
},
},
});
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
```
**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 {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* form fields */}
</form>
</Form>
);
}
```
### 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 (
<AuthContext.Provider value={{ user, session, loading }}>
{children}
</AuthContext.Provider>
);
}
```
**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 (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
```
**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
<AdminPageLayout
title="User Management"
description="Manage users"
onRefresh={handleRefresh}
refreshMode="auto"
pollInterval={30000}
>
{children}
</AdminPageLayout>
```
**LoadingGate** - Handles loading/error states
```typescript
<LoadingGate
isLoading={loading}
error={error}
variant="skeleton"
>
<Content />
</LoadingGate>
```
**ProfileBadge** - User display with role badges
```typescript
<ProfileBadge
username="john"
displayName="John Doe"
role="moderator"
showRole
size="md"
/>
```
---
## 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<ImageAssignments>({
banner: null,
card: null,
uploaded: []
});
<EntityMultiImageUploader
value={imageAssignments}
onChange={setImageAssignments}
entityType="park"
maxImages={20}
/>
```
---
## 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'));
<Suspense fallback={<LoadingGate isLoading />}>
<Route path="/admin" element={<AdminDashboard />} />
</Suspense>
```
### 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

501
docs/TESTING_GUIDE.md Normal file
View File

@@ -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

614
docs/TROUBLESHOOTING.md Normal file
View File

@@ -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