Convert milliseconds timestamps from Discord bot to datetime objects for PostgreSQL DateTimeField columns in notifications, packs, paperdex, and rewards routers. Also fix rewards GET created_after filter. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
620 lines
21 KiB
JSON
620 lines
21 KiB
JSON
{
|
|
"meta": {
|
|
"version": "1.2.0",
|
|
"created": "2026-01-25",
|
|
"lastUpdated": "2026-01-30",
|
|
"planType": "migration",
|
|
"description": "SQLite to PostgreSQL migration for Paper Dynasty database API",
|
|
"branch": "postgres-migration",
|
|
"totalEstimatedHours": 26,
|
|
"totalTasks": 28,
|
|
"completedTasks": 18
|
|
},
|
|
"context": {
|
|
"sourceDatabase": {
|
|
"type": "SQLite",
|
|
"file": "storage/pd_master.db",
|
|
"size": "110 MB",
|
|
"tables": 29,
|
|
"totalRecords": 515000,
|
|
"largestTable": {
|
|
"name": "stratplay",
|
|
"records": 332737
|
|
}
|
|
},
|
|
"targetDatabase": {
|
|
"type": "PostgreSQL 17",
|
|
"server": "sba_postgres (same server as Major Domo)",
|
|
"database": "pd_master",
|
|
"user": "pd_admin",
|
|
"credentialsFile": ".secrets/pd_admin_credentials.txt"
|
|
},
|
|
"lessonsFromMajorDomo": [
|
|
"CRITICAL: Primary key IDs must be explicitly preserved during migration",
|
|
"PostgreSQL GROUP BY requires ALL non-aggregated columns",
|
|
"Boolean fields cannot be summed directly - cast to integer first",
|
|
"Discord snowflake IDs must be strings, not integers (N/A for Paper Dynasty)",
|
|
"VARCHAR fields need explicit max_length",
|
|
"NULL constraints are stricter in PostgreSQL",
|
|
"Foreign key orphaned records need smart fallback handling",
|
|
"Reset sequences after ID-preserving inserts"
|
|
],
|
|
"devServer": {
|
|
"access": "ssh sba-db",
|
|
"composeLocation": "cd container-data/dev-sba-database/"
|
|
}
|
|
},
|
|
"categories": {
|
|
"critical": "Must complete before migration - blocks production",
|
|
"high": "Required for successful migration",
|
|
"medium": "Improves migration quality/reliability",
|
|
"low": "Polish and nice-to-have",
|
|
"completed": "Already done on postgres-migration branch"
|
|
},
|
|
"tasks": [
|
|
{
|
|
"id": "MIG-001",
|
|
"name": "Environment-based database configuration",
|
|
"description": "Add PostgreSQL support with environment variable switching between SQLite/PostgreSQL",
|
|
"category": "completed",
|
|
"priority": 1,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "app/db_engine.py",
|
|
"lines": [11, 35],
|
|
"issue": "Now supports DATABASE_TYPE env var for SQLite/PostgreSQL switching"
|
|
}
|
|
],
|
|
"suggestedFix": "Already implemented with PooledPostgresqlDatabase",
|
|
"estimatedHours": 2,
|
|
"notes": "Includes connection pooling (20 max, 5-min stale timeout, autorollback)"
|
|
},
|
|
{
|
|
"id": "MIG-002",
|
|
"name": "Add table_name to all models",
|
|
"description": "Explicit table naming for PostgreSQL compatibility",
|
|
"category": "completed",
|
|
"priority": 2,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "app/db_engine.py",
|
|
"lines": [],
|
|
"issue": "All 29 models now have Meta.table_name defined"
|
|
}
|
|
],
|
|
"suggestedFix": "Already implemented",
|
|
"estimatedHours": 1,
|
|
"notes": "Prevents Peewee naming inconsistencies"
|
|
},
|
|
{
|
|
"id": "MIG-003",
|
|
"name": "Fix GROUP BY queries for PostgreSQL",
|
|
"description": "PostgreSQL requires all non-aggregated SELECT fields in GROUP BY clause",
|
|
"category": "completed",
|
|
"priority": 3,
|
|
"completed": true,
|
|
"tested": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "app/routers_v2/stratplays.py",
|
|
"lines": [342, 456, 645, 733],
|
|
"issue": "Conditionally build SELECT fields based on group_by parameter"
|
|
}
|
|
],
|
|
"suggestedFix": "Already implemented - needs testing with all group_by variations",
|
|
"estimatedHours": 4,
|
|
"notes": "Pattern: only include non-aggregated fields that will be in GROUP BY"
|
|
},
|
|
{
|
|
"id": "MIG-004",
|
|
"name": "Add psycopg2-binary dependency",
|
|
"description": "PostgreSQL adapter for Python",
|
|
"category": "completed",
|
|
"priority": 4,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "requirements.txt",
|
|
"lines": [],
|
|
"issue": "psycopg2-binary added"
|
|
}
|
|
],
|
|
"suggestedFix": "Already implemented",
|
|
"estimatedHours": 0.1,
|
|
"notes": ""
|
|
},
|
|
{
|
|
"id": "MIG-005",
|
|
"name": "Docker Compose for local testing",
|
|
"description": "Local PostgreSQL environment for development testing",
|
|
"category": "completed",
|
|
"priority": 5,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "docker-compose.yml",
|
|
"lines": [],
|
|
"issue": "PostgreSQL 17 + Adminer configured"
|
|
},
|
|
{
|
|
"path": "QUICK_START.md",
|
|
"lines": [],
|
|
"issue": "Testing guide created"
|
|
}
|
|
],
|
|
"suggestedFix": "Already implemented",
|
|
"estimatedHours": 1,
|
|
"notes": "Adminer on port 8081"
|
|
},
|
|
{
|
|
"id": "MIG-006",
|
|
"name": "Migration script auto-detection",
|
|
"description": "db_migrations.py auto-selects PostgresqlMigrator or SqliteMigrator",
|
|
"category": "completed",
|
|
"priority": 6,
|
|
"completed": true,
|
|
"tested": false,
|
|
"dependencies": ["MIG-001"],
|
|
"files": [
|
|
{
|
|
"path": "db_migrations.py",
|
|
"lines": [],
|
|
"issue": "Migrator selection based on DATABASE_TYPE"
|
|
}
|
|
],
|
|
"suggestedFix": "Already implemented",
|
|
"estimatedHours": 0.5,
|
|
"notes": ""
|
|
},
|
|
{
|
|
"id": "MIG-007",
|
|
"name": "Create data migration script with ID preservation",
|
|
"description": "CRITICAL: Migrate all data from SQLite to PostgreSQL while preserving primary key IDs exactly",
|
|
"category": "critical",
|
|
"priority": 1,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["MIG-001", "MIG-002"],
|
|
"files": [
|
|
{
|
|
"path": "scripts/migrate_to_postgres.py",
|
|
"lines": [],
|
|
"issue": "New file - must explicitly insert IDs and reset sequences"
|
|
}
|
|
],
|
|
"suggestedFix": "1. Read all records from SQLite\n2. Insert into PostgreSQL with explicit ID values\n3. Reset PostgreSQL sequences: SELECT setval('table_id_seq', MAX(id))\n4. Validate record counts match\n5. Smart FK error handling (batch insert with individual fallback)",
|
|
"estimatedHours": 3,
|
|
"notes": "Major Domo's #1 lesson: Without explicit ID preservation, PostgreSQL auto-assigns sequential IDs starting from 1, causing all FK references to point to wrong records"
|
|
},
|
|
{
|
|
"id": "MIG-008",
|
|
"name": "Fix on_conflict_replace() calls (Player model)",
|
|
"description": "Convert SQLite on_conflict_replace() to PostgreSQL on_conflict() for Player model",
|
|
"category": "critical",
|
|
"priority": 2,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "main.py",
|
|
"lines": [1696],
|
|
"issue": "Player.insert_many(batch).on_conflict_replace()"
|
|
},
|
|
{
|
|
"path": "app/routers_v2/players.py",
|
|
"lines": [808],
|
|
"issue": "Player.insert_many(batch).on_conflict_replace()"
|
|
}
|
|
],
|
|
"suggestedFix": "Player.insert_many(batch).on_conflict(\n conflict_target=[Player.player_id],\n action='update',\n update={Player.p_name: EXCLUDED.p_name, ...all fields}\n).execute()",
|
|
"estimatedHours": 0.5,
|
|
"notes": "Player has explicit player_id primary key - straightforward"
|
|
},
|
|
{
|
|
"id": "MIG-009",
|
|
"name": "Fix on_conflict_replace() calls (Card models)",
|
|
"description": "Convert SQLite on_conflict_replace() for BattingCard, PitchingCard, CardPosition, ratings",
|
|
"category": "critical",
|
|
"priority": 3,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "app/routers_v2/battingcards.py",
|
|
"lines": [134],
|
|
"issue": "BattingCard - unique on (player, variant)"
|
|
},
|
|
{
|
|
"path": "app/routers_v2/pitchingcards.py",
|
|
"lines": [130],
|
|
"issue": "PitchingCard - unique on (player, variant)"
|
|
},
|
|
{
|
|
"path": "app/routers_v2/cardpositions.py",
|
|
"lines": [131],
|
|
"issue": "CardPosition - unique on (player, variant, position)"
|
|
},
|
|
{
|
|
"path": "app/routers_v2/battingcardratings.py",
|
|
"lines": [549],
|
|
"issue": "BattingCardRatings - unique on (battingcard, vs_hand)"
|
|
},
|
|
{
|
|
"path": "app/routers_v2/pitchingcardratings.py",
|
|
"lines": [432],
|
|
"issue": "PitchingCardRatings - unique on (pitchingcard, vs_hand)"
|
|
}
|
|
],
|
|
"suggestedFix": "All have existing unique indexes - use those as conflict_target",
|
|
"estimatedHours": 2,
|
|
"notes": "These have many fields to update - consider helper function"
|
|
},
|
|
{
|
|
"id": "MIG-010",
|
|
"name": "Fix on_conflict_replace() calls (Game models)",
|
|
"description": "Convert SQLite on_conflict_replace() for StratPlay, Decision, GauntletReward",
|
|
"category": "critical",
|
|
"priority": 4,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["MIG-011"],
|
|
"files": [
|
|
{
|
|
"path": "app/routers_v2/stratplays.py",
|
|
"lines": [1082],
|
|
"issue": "StratPlay - needs unique index on (game, play_num)"
|
|
},
|
|
{
|
|
"path": "app/routers_v2/decisions.py",
|
|
"lines": [217],
|
|
"issue": "Decision - needs unique index on (game, pitcher)"
|
|
},
|
|
{
|
|
"path": "main.py",
|
|
"lines": [4978],
|
|
"issue": "GauntletReward - investigate if id provided or needs refactor"
|
|
},
|
|
{
|
|
"path": "app/routers_v2/gauntletrewards.py",
|
|
"lines": [127],
|
|
"issue": "GauntletReward - same as main.py"
|
|
}
|
|
],
|
|
"suggestedFix": "Add unique indexes first (MIG-011), then implement on_conflict()",
|
|
"estimatedHours": 1.5,
|
|
"notes": "StratPlay and Decision need new unique indexes to be created first"
|
|
},
|
|
{
|
|
"id": "MIG-011",
|
|
"name": "Add missing unique indexes for upserts",
|
|
"description": "Create unique indexes needed for PostgreSQL on_conflict() operations",
|
|
"category": "high",
|
|
"priority": 5,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "app/db_engine.py",
|
|
"lines": [779, 848],
|
|
"issue": "Add unique indexes for StratPlay and Decision"
|
|
}
|
|
],
|
|
"suggestedFix": "StratPlay: ModelIndex(StratPlay, (StratPlay.game, StratPlay.play_num), unique=True)\nDecision: ModelIndex(Decision, (Decision.game, Decision.pitcher), unique=True)",
|
|
"estimatedHours": 1,
|
|
"notes": "These are natural business keys - a play number should be unique within a game, and a pitcher should have one decision per game"
|
|
},
|
|
{
|
|
"id": "MIG-012",
|
|
"name": "Fix on_conflict_replace() for MlbPlayer",
|
|
"description": "Convert or remove on_conflict_replace() for MlbPlayer",
|
|
"category": "medium",
|
|
"priority": 6,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "app/routers_v2/mlbplayers.py",
|
|
"lines": [185],
|
|
"issue": "MlbPlayer.insert_many(batch).on_conflict_replace()"
|
|
}
|
|
],
|
|
"suggestedFix": "Code already checks for duplicates before insert (lines 170-179) and raises HTTPException. The on_conflict_replace() may be unnecessary. Option 1: Remove it and use plain insert_many(). Option 2: Use on_conflict with id as target.",
|
|
"estimatedHours": 0.25,
|
|
"notes": "Low risk - pre-check rejects duplicates"
|
|
},
|
|
{
|
|
"id": "MIG-013",
|
|
"name": "Fix boolean comparison in teams.py",
|
|
"description": "PostgreSQL requires True/False instead of 1/0 for boolean comparisons",
|
|
"category": "low",
|
|
"priority": 7,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "app/routers_v2/teams.py",
|
|
"lines": [110, 112],
|
|
"issue": "Team.has_guide == 0 / Team.has_guide == 1"
|
|
}
|
|
],
|
|
"suggestedFix": "Change to Team.has_guide == False / Team.has_guide == True",
|
|
"estimatedHours": 0.25,
|
|
"notes": "Peewee may handle this automatically, but explicit is better"
|
|
},
|
|
{
|
|
"id": "MIG-014",
|
|
"name": "SQLite data integrity audit",
|
|
"description": "Check for NULL values, orphaned FKs, VARCHAR lengths before migration",
|
|
"category": "high",
|
|
"priority": 8,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "scripts/audit_sqlite.py",
|
|
"lines": [],
|
|
"issue": "New file - pre-migration data validation"
|
|
}
|
|
],
|
|
"suggestedFix": "Create script to check:\n1. NULL values in NOT NULL fields\n2. Orphaned foreign key records\n3. VARCHAR field max lengths\n4. Table record counts for baseline",
|
|
"estimatedHours": 1.5,
|
|
"notes": "Major Domo found 206 orphaned decisions and VARCHAR violations"
|
|
},
|
|
{
|
|
"id": "MIG-015",
|
|
"name": "Test on dev PostgreSQL server",
|
|
"description": "Full migration test on sba-db dev server with production data copy",
|
|
"category": "high",
|
|
"priority": 9,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["MIG-007", "MIG-008", "MIG-009", "MIG-010", "MIG-014"],
|
|
"files": [],
|
|
"suggestedFix": "1. ssh sba-db\n2. Create pd_master database with pd_admin user\n3. Copy production SQLite to dev\n4. Run migration script\n5. Verify record counts\n6. Test API endpoints",
|
|
"estimatedHours": 3,
|
|
"notes": "Dev server access: ssh sba-db, then cd container-data/dev-sba-database/"
|
|
},
|
|
{
|
|
"id": "MIG-016",
|
|
"name": "Production migration execution",
|
|
"description": "Execute migration on production server within maintenance window",
|
|
"category": "critical",
|
|
"priority": 10,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["MIG-015"],
|
|
"files": [],
|
|
"suggestedFix": "1. Notify users of maintenance window\n2. Stop Paper Dynasty API\n3. Create SQLite backup\n4. Create pd_master database and pd_admin user\n5. Run migration script\n6. Verify data integrity\n7. Update docker-compose.yml with PostgreSQL env vars\n8. Start API\n9. Smoke test critical endpoints\n10. Announce migration complete",
|
|
"estimatedHours": 3,
|
|
"notes": "Downtime window: 1-4 hours. Have rollback plan ready."
|
|
}
|
|
],
|
|
"quickWins": [
|
|
{
|
|
"taskId": "MIG-013",
|
|
"estimatedMinutes": 15,
|
|
"impact": "Prevents boolean comparison issues in team queries"
|
|
},
|
|
{
|
|
"taskId": "MIG-012",
|
|
"estimatedMinutes": 15,
|
|
"impact": "Simplify MlbPlayer insert logic"
|
|
}
|
|
],
|
|
"productionBlockers": [
|
|
{
|
|
"taskId": "MIG-007",
|
|
"reason": "Without ID-preserving migration, all foreign key references will break"
|
|
},
|
|
{
|
|
"taskId": "MIG-008",
|
|
"reason": "Player upserts will fail without PostgreSQL-compatible syntax"
|
|
},
|
|
{
|
|
"taskId": "MIG-009",
|
|
"reason": "Card data upserts will fail without PostgreSQL-compatible syntax"
|
|
},
|
|
{
|
|
"taskId": "MIG-010",
|
|
"reason": "Game data upserts will fail without PostgreSQL-compatible syntax"
|
|
}
|
|
],
|
|
"weeklyRoadmap": {
|
|
"week1": {
|
|
"theme": "Code Changes - Make PostgreSQL Compatible",
|
|
"tasks": ["MIG-007", "MIG-008", "MIG-009", "MIG-010", "MIG-011", "MIG-012", "MIG-013"],
|
|
"estimatedHours": 8.5
|
|
},
|
|
"week2": {
|
|
"theme": "Testing & Validation",
|
|
"tasks": ["MIG-014", "MIG-015"],
|
|
"estimatedHours": 4.5
|
|
},
|
|
"week3": {
|
|
"theme": "Production Migration",
|
|
"tasks": ["MIG-016"],
|
|
"estimatedHours": 3
|
|
}
|
|
},
|
|
"timestampFixTasks": {
|
|
"description": "Post-migration timestamp type mismatch fixes - Discord bot sends milliseconds integers but PostgreSQL uses DateTimeField",
|
|
"pattern": {
|
|
"postHandler": "datetime.fromtimestamp(field / 1000)",
|
|
"getFilter": "param_dt = datetime.fromtimestamp(param / 1000)",
|
|
"nullableField": "Use None instead of 0",
|
|
"nullCheck": "Model.field.is_null() instead of == 0"
|
|
},
|
|
"tasks": [
|
|
{
|
|
"id": "TS-001",
|
|
"name": "Fix rewards.py GET created_after filter",
|
|
"category": "critical",
|
|
"completed": true,
|
|
"file": "app/routers_v2/rewards.py",
|
|
"lines": [46, 47],
|
|
"notes": "Fixed 2026-01-30. Was causing /comeonmanineedthis to fail."
|
|
},
|
|
{
|
|
"id": "TS-002",
|
|
"name": "Fix notifications.py POST created field",
|
|
"category": "critical",
|
|
"completed": true,
|
|
"file": "app/routers_v2/notifications.py",
|
|
"lines": [23, 115],
|
|
"botUsage": ["helpers/main.py:524", "helpers/main.py:779"],
|
|
"notes": "Breaks rare card pull notifications"
|
|
},
|
|
{
|
|
"id": "TS-003",
|
|
"name": "Fix notifications.py GET created_after filter",
|
|
"category": "critical",
|
|
"completed": false,
|
|
"file": "app/routers_v2/notifications.py",
|
|
"lines": [34, 44],
|
|
"notes": "Used for notification polling"
|
|
},
|
|
{
|
|
"id": "TS-004",
|
|
"name": "Fix packs.py POST open_time field",
|
|
"category": "critical",
|
|
"completed": true,
|
|
"file": "app/routers_v2/packs.py",
|
|
"lines": [29, 158, 184],
|
|
"botUsage": ["cogs/economy.py:1287", "cogs/economy_new/team_setup.py:242", "cogs/economy_new/admin_tools.py:76,115,147"],
|
|
"notes": "Breaks starter pack creation and admin pack distribution"
|
|
},
|
|
{
|
|
"id": "TS-005",
|
|
"name": "Fix packs.py PATCH open_time field",
|
|
"category": "critical",
|
|
"completed": true,
|
|
"file": "app/routers_v2/packs.py",
|
|
"lines": [199, 230, 234],
|
|
"notes": "Used when updating pack open times"
|
|
},
|
|
{
|
|
"id": "TS-006",
|
|
"name": "Fix paperdex.py POST created field",
|
|
"category": "critical",
|
|
"completed": true,
|
|
"file": "app/routers_v2/paperdex.py",
|
|
"lines": [26, 121],
|
|
"botUsage": ["api_calls.py:post_to_dex()"],
|
|
"notes": "Breaks collection tracking"
|
|
},
|
|
{
|
|
"id": "TS-007",
|
|
"name": "Fix paperdex.py GET created_after/before filters",
|
|
"category": "high",
|
|
"completed": false,
|
|
"file": "app/routers_v2/paperdex.py",
|
|
"lines": [31, 32, 48, 50],
|
|
"notes": "Used for filtering paperdex by date range"
|
|
},
|
|
{
|
|
"id": "TS-008",
|
|
"name": "Fix gauntletruns.py GET timestamp filters (4 fields)",
|
|
"category": "high",
|
|
"completed": false,
|
|
"file": "app/routers_v2/gauntletruns.py",
|
|
"lines": [36, 37, 58, 60, 62, 64],
|
|
"notes": "created_after, created_before, ended_after, ended_before"
|
|
},
|
|
{
|
|
"id": "TS-009",
|
|
"name": "Fix gauntletruns.py GET is_active filter (NULL vs 0)",
|
|
"category": "high",
|
|
"completed": false,
|
|
"file": "app/routers_v2/gauntletruns.py",
|
|
"lines": [67, 69],
|
|
"notes": "Should use is_null() not == 0"
|
|
},
|
|
{
|
|
"id": "TS-010",
|
|
"name": "Fix gauntletruns.py PATCH timestamp fields",
|
|
"category": "high",
|
|
"completed": false,
|
|
"file": "app/routers_v2/gauntletruns.py",
|
|
"lines": [122, 124, 129, 131],
|
|
"notes": "Assigns int to DateTimeField, uses 0 instead of None"
|
|
},
|
|
{
|
|
"id": "TS-011",
|
|
"name": "Fix gauntletruns.py POST timestamp fields",
|
|
"category": "high",
|
|
"completed": false,
|
|
"file": "app/routers_v2/gauntletruns.py",
|
|
"lines": [28, 29, 152],
|
|
"notes": "Pydantic model uses int with default 0"
|
|
},
|
|
{
|
|
"id": "TS-012",
|
|
"name": "Fix batstats.py GET created filter",
|
|
"category": "medium",
|
|
"completed": false,
|
|
"file": "app/routers_v2/batstats.py",
|
|
"lines": [74, 98],
|
|
"notes": "Not actively used - commented out in gameplay_legacy.py"
|
|
},
|
|
{
|
|
"id": "TS-013",
|
|
"name": "Fix pitstats.py GET created filter",
|
|
"category": "medium",
|
|
"completed": false,
|
|
"file": "app/routers_v2/pitstats.py",
|
|
"lines": [60, 85],
|
|
"notes": "Not actively used - commented out in gameplay_legacy.py"
|
|
}
|
|
],
|
|
"implementationOrder": [
|
|
{"phase": 1, "name": "Critical POST Endpoints", "tasks": ["TS-002", "TS-004", "TS-005", "TS-006"]},
|
|
{"phase": 2, "name": "Critical GET Filters", "tasks": ["TS-003", "TS-007"]},
|
|
{"phase": 3, "name": "Gauntlet System", "tasks": ["TS-008", "TS-009", "TS-010", "TS-011"]},
|
|
{"phase": 4, "name": "Stats Endpoints", "tasks": ["TS-012", "TS-013"]}
|
|
]
|
|
},
|
|
"rollbackPlan": {
|
|
"triggers": [
|
|
"Data corruption detected",
|
|
"More than 5% of endpoints failing",
|
|
"Performance more than 5x worse than SQLite",
|
|
"Critical functionality broken"
|
|
],
|
|
"duringTesting": {
|
|
"steps": [
|
|
"Set DATABASE_TYPE=sqlite",
|
|
"API immediately uses SQLite",
|
|
"No data loss - PostgreSQL was a copy"
|
|
]
|
|
},
|
|
"afterProduction": {
|
|
"steps": [
|
|
"Stop API: docker-compose down",
|
|
"Update docker-compose.yml: DATABASE_TYPE=sqlite",
|
|
"Restore SQLite backup if needed",
|
|
"Start API: docker-compose up -d",
|
|
"Verify SQLite connectivity",
|
|
"Document issues for retry"
|
|
],
|
|
"timeLimit": "24 hours from migration"
|
|
}
|
|
}
|
|
}
|