diff --git a/docs/versioning/MIGRATION.md b/docs/versioning/MIGRATION.md index 1ed42968..17de63eb 100644 --- a/docs/versioning/MIGRATION.md +++ b/docs/versioning/MIGRATION.md @@ -4,7 +4,7 @@ ## Status -✅ **Migration Complete** - The relational versioning system is now active. This guide documents the migration for reference. +✅ **Migration Complete** - The relational versioning system is now fully active. Legacy system has been removed. ## Overview @@ -25,72 +25,53 @@ The new system uses dedicated relational tables: 1. **Phase 1: Create relational tables** ✅ Complete 2. **Phase 2: Enable triggers** ✅ Complete 3. **Phase 3: Dual-write period** ✅ Complete -4. **Phase 4: Backfill historical data** ⏸️ Optional -5. **Phase 5: Monitor** 🔄 Ongoing -6. **Phase 6: Deprecate JSONB table** 📅 Future +4. **Phase 4: Archive legacy data** ✅ Complete +5. **Phase 5: Drop legacy tables & functions** ✅ Complete +6. **Phase 6: Update documentation & code** ✅ Complete ## Current State -- ✅ All new versions written to relational tables +- ✅ All versions written to relational tables - ✅ Triggers active on all entity tables - ✅ Automated cleanup scheduled via pg_cron -- ⚠️ Old `entity_versions` table retained for backward compatibility -- ⚠️ `src/lib/versioningHelpers.ts` deprecated but not removed (scheduled for removal: 2025-12-01) +- ✅ Old `entity_versions` table archived to `entity_versions_archive` +- ✅ Legacy JSONB tables and functions removed +- ✅ `src/lib/versioningHelpers.ts` removed +- ✅ All code updated to use relational system -## Migration Timeline +## Completed Migration Phases -### ✅ Phase 1: New System Deployed (Completed) +### ✅ Phase 1: New System Deployed - Relational version tables created (`park_versions`, `ride_versions`, etc.) - Triggers enabled on all entity tables - RLS policies active and tested - Frontend integrated with new hooks - Complete documentation suite created -### 🟡 Phase 2: Parallel Operation (Current - Days 1-30) -- Both old and new systems exist side-by-side -- New triggers create versions in relational tables -- Old JSONB table receives no new data -- Monitoring for issues and edge cases -- `versioningHelpers.ts` marked as deprecated +### ✅ Phase 2: Parallel Operation +- Both old and new systems existed side-by-side +- New triggers created versions in relational tables +- Old JSONB table received no new data +- Monitored for issues and edge cases -**Action Items:** -- [ ] Monitor version creation in new tables -- [ ] Verify no new inserts to old `entity_versions` table -- [ ] Search codebase for deprecated function usage -- [ ] Collect feedback from team +### ✅ Phase 3: Archive Legacy Data +- Archived old `entity_versions` to `entity_versions_archive` +- Verified data integrity and counts matched +- Archive table retained for audit purposes -### 🔵 Phase 3: Archive Legacy Data (Day 30) -- Archive old `entity_versions` to `entity_versions_archive` -- Verify data integrity and counts match -- Keep archive for 60 more days as safety net -- Document archive location and access procedures +### ✅ Phase 4: Drop Legacy System +- Dropped old `entity_versions` table +- Dropped old RPC functions (`create_entity_version`, `compare_versions`, etc.) +- Dropped `auto_create_entity_version()` trigger function +- Removed `src/lib/versioningHelpers.ts` file +- Updated all code to use relational system only +- Updated documentation -**SQL Migration:** -```sql --- See supabase/migrations/*_archive_legacy_versions.sql -CREATE TABLE entity_versions_archive (LIKE entity_versions INCLUDING ALL); -INSERT INTO entity_versions_archive SELECT * FROM entity_versions; -``` - -### 🟢 Phase 4: Drop Legacy Tables (Day 90) -- Drop old `entity_versions` table -- Drop old RPC functions (`create_entity_version`, `compare_versions`, etc.) -- Remove `src/lib/versioningHelpers.ts` file -- Remove archive table (or retain indefinitely for audit) -- Update all documentation to remove references to old system - -**SQL Migration:** -```sql --- See supabase/migrations/*_drop_legacy_versions.sql -DROP TABLE entity_versions CASCADE; -DROP FUNCTION create_entity_version(...); -``` - -### 🚀 Phase 5: Optimization (Ongoing) -- Automated cleanup via pg_cron (monthly) -- Performance monitoring and index tuning +### 🔄 Phase 5: Ongoing Optimization +- Automated cleanup via pg_cron runs monthly +- Performance monitoring and index tuning as needed - Documentation updates based on usage patterns -- Version retention policy adjustments as needed +- Version retention policy: 50 versions per entity (configurable) ## Backfill Script (Optional) @@ -134,27 +115,31 @@ WHERE ev.entity_type = 'park' ON CONFLICT DO NOTHING; ``` -## Cleanup (Future) +## What Was Removed -When ready to fully deprecate JSONB system: +The following tables, functions, and files have been permanently removed: -```sql --- 1. Verify all versions migrated -SELECT COUNT(*) FROM entity_versions; -- Should match relational tables +**Tables:** +- `entity_versions` (main JSONB table) +- `entity_field_history` (field-level change tracking) +- `entity_relationships_history` (relationship tracking) +- `version_diffs` (pre-computed diffs) --- 2. Drop old table (IRREVERSIBLE) -DROP TABLE IF EXISTS entity_versions CASCADE; +**Functions:** +- `create_entity_version()` - Created JSONB versions +- `compare_versions()` - Compared JSONB versions +- `create_field_history_entries()` - Created field history +- `auto_create_entity_version()` - Automatic versioning trigger --- 3. Remove deprecated helpers --- Delete src/lib/versioningHelpers.ts -``` +**Files:** +- `src/lib/versioningHelpers.ts` - Old helper functions -## Rollback Plan +**Archived:** +- `entity_versions_archive` - Contains historical JSONB data for audit purposes -If issues arise, rollback steps: +## Field-Level History -1. Disable triggers on entity tables -2. Revert edge functions to use old JSONB system -3. Keep relational tables for future retry - -**Note:** Not recommended - new system is production-ready. +Field-level history tracking (`entity_field_history` table) has been removed. To view field changes: +- Use the **Version Comparison** feature in the Version History tab +- Compare any two versions to see all field-level changes +- The `FieldHistoryDialog` component now shows a helpful message directing users to version comparison diff --git a/src/lib/systemActivityService.ts b/src/lib/systemActivityService.ts index e56a6c43..b3820ed7 100644 --- a/src/lib/systemActivityService.ts +++ b/src/lib/systemActivityService.ts @@ -137,19 +137,20 @@ export async function fetchSystemActivities( // Process park versions if (!parkVersions.error && parkVersions.data) { for (const version of parkVersions.data) { + const parkVersion = version as any; // Type assertion for relational version structure activities.push({ - id: version.version_id, + id: parkVersion.version_id, type: 'entity_change', - timestamp: version.created_at, - actor_id: version.created_by || null, - action: `${version.change_type} park`, + timestamp: parkVersion.created_at, + actor_id: parkVersion.created_by || null, + action: `${parkVersion.change_type} park`, details: { entity_type: 'park', - entity_id: version.park_id, - entity_name: version.name, - change_type: version.change_type, - change_reason: version.change_reason, - version_number: version.version_number, + entity_id: parkVersion.park_id, + entity_name: parkVersion.name, + change_type: parkVersion.change_type, + change_reason: parkVersion.change_reason, + version_number: parkVersion.version_number, } as EntityChangeDetails, }); } @@ -158,19 +159,20 @@ export async function fetchSystemActivities( // Process ride versions if (!rideVersions.error && rideVersions.data) { for (const version of rideVersions.data) { + const rideVersion = version as any; activities.push({ - id: version.version_id, + id: rideVersion.version_id, type: 'entity_change', - timestamp: version.created_at, - actor_id: version.created_by || null, - action: `${version.change_type} ride`, + timestamp: rideVersion.created_at, + actor_id: rideVersion.created_by || null, + action: `${rideVersion.change_type} ride`, details: { entity_type: 'ride', - entity_id: version.ride_id, - entity_name: version.name, - change_type: version.change_type, - change_reason: version.change_reason, - version_number: version.version_number, + entity_id: rideVersion.ride_id, + entity_name: rideVersion.name, + change_type: rideVersion.change_type, + change_reason: rideVersion.change_reason, + version_number: rideVersion.version_number, } as EntityChangeDetails, }); } @@ -179,19 +181,20 @@ export async function fetchSystemActivities( // Process company versions if (!companyVersions.error && companyVersions.data) { for (const version of companyVersions.data) { + const companyVersion = version as any; activities.push({ - id: version.version_id, + id: companyVersion.version_id, type: 'entity_change', - timestamp: version.created_at, - actor_id: version.created_by || null, - action: `${version.change_type} company`, + timestamp: companyVersion.created_at, + actor_id: companyVersion.created_by || null, + action: `${companyVersion.change_type} company`, details: { entity_type: 'company', - entity_id: version.company_id, - entity_name: version.name, - change_type: version.change_type, - change_reason: version.change_reason, - version_number: version.version_number, + entity_id: companyVersion.company_id, + entity_name: companyVersion.name, + change_type: companyVersion.change_type, + change_reason: companyVersion.change_reason, + version_number: companyVersion.version_number, } as EntityChangeDetails, }); } @@ -200,19 +203,20 @@ export async function fetchSystemActivities( // Process ride model versions if (!modelVersions.error && modelVersions.data) { for (const version of modelVersions.data) { + const modelVersion = version as any; activities.push({ - id: version.version_id, + id: modelVersion.version_id, type: 'entity_change', - timestamp: version.created_at, - actor_id: version.created_by || null, - action: `${version.change_type} ride_model`, + timestamp: modelVersion.created_at, + actor_id: modelVersion.created_by || null, + action: `${modelVersion.change_type} ride_model`, details: { entity_type: 'ride_model', - entity_id: version.ride_model_id, - entity_name: version.name, - change_type: version.change_type, - change_reason: version.change_reason, - version_number: version.version_number, + entity_id: modelVersion.ride_model_id, + entity_name: modelVersion.name, + change_type: modelVersion.change_type, + change_reason: modelVersion.change_reason, + version_number: modelVersion.version_number, } as EntityChangeDetails, }); } diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index 76cfb367..3ab702b3 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -50,14 +50,18 @@ export default function AdminDashboard() { const checkSuspiciousVersions = useCallback(async () => { if (!user || !isModerator()) return; - const { count, error } = await supabase - .from('entity_versions') - .select('*', { count: 'exact', head: true }) - .is('changed_by', null); + // Query all version tables for suspicious entries (no changed_by) + const queries = [ + supabase.from('park_versions').select('*', { count: 'exact', head: true }).is('created_by', null), + supabase.from('ride_versions').select('*', { count: 'exact', head: true }).is('created_by', null), + supabase.from('company_versions').select('*', { count: 'exact', head: true }).is('created_by', null), + supabase.from('ride_model_versions').select('*', { count: 'exact', head: true }).is('created_by', null), + ]; - if (!error && count !== null) { - setSuspiciousVersionsCount(count); - } + const results = await Promise.all(queries); + const totalCount = results.reduce((sum, result) => sum + (result.count || 0), 0); + + setSuspiciousVersionsCount(totalCount); }, [user, isModerator]); useEffect(() => { diff --git a/supabase/migrations/20251015182852_dd6e0b6c-8986-4849-b9b9-ecdc062ba9bc.sql b/supabase/migrations/20251015182852_dd6e0b6c-8986-4849-b9b9-ecdc062ba9bc.sql new file mode 100644 index 00000000..8fa2f18a --- /dev/null +++ b/supabase/migrations/20251015182852_dd6e0b6c-8986-4849-b9b9-ecdc062ba9bc.sql @@ -0,0 +1,3 @@ +-- Drop legacy versioning function that references deleted tables +-- This function calls create_entity_version() which was removed in the previous migration +DROP FUNCTION IF EXISTS public.auto_create_entity_version() CASCADE; \ No newline at end of file