mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
Add documentation to files
This commit is contained in:
167
docs/ARCHITECTURE_OVERVIEW.md
Normal file
167
docs/ARCHITECTURE_OVERVIEW.md
Normal 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
|
||||||
636
docs/DATABASE_ARCHITECTURE.md
Normal file
636
docs/DATABASE_ARCHITECTURE.md
Normal 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
446
docs/DEPLOYMENT.md
Normal 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
|
||||||
658
docs/FRONTEND_ARCHITECTURE.md
Normal file
658
docs/FRONTEND_ARCHITECTURE.md
Normal 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
501
docs/TESTING_GUIDE.md
Normal 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
614
docs/TROUBLESHOOTING.md
Normal 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
|
||||||
Reference in New Issue
Block a user