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