diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md deleted file mode 100644 index 9100be90..00000000 --- a/DEPLOYMENT_GUIDE.md +++ /dev/null @@ -1,436 +0,0 @@ -# Deployment Guide - Roots of The Valley - -**Target Server:** lotor.dc3.crunchtools.com:22422 -**Service:** rootsofthevalley.org -**Container Registry:** quay.io/crunchtools/rotv:latest - ---- - -## Quick Deployment (Standard Process) - -### Prerequisites -- [ ] PR merged to master -- [ ] GitHub Actions build completed successfully -- [ ] All tests passing (including integration tests) -- [ ] No blocking issues identified - -### Deployment Steps - -```bash -# 1. SSH to production -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# 2. Backup database -mkdir -p /root/backups -podman exec rootsofthevalley.org pg_dump -U postgres rotv > \ - /root/backups/rotv_$(date +%Y%m%d_%H%M%S).sql - -# 3. Pull latest image -podman pull quay.io/crunchtools/rotv:latest - -# 4. Restart service -systemctl restart rootsofthevalley.org - -# 5. Verify deployment (30-second health check) -sleep 10 -systemctl status rootsofthevalley.org -curl -sf https://rootsofthevalley.org/api/health && echo "✅ Healthy" || echo "❌ Failed" -``` - -### Post-Deployment Verification - -```bash -# Option 1: Automated verification (recommended) -bash scripts/post-deployment-report.sh - -# Option 2: Run smoke tests via GitHub Actions -gh workflow run smoke-test.yml - -# Option 3: Manual verification -curl https://rootsofthevalley.org/api/pois/1/media | jq -``` - ---- - -## Deployment with Migrations (e.g., PR #182) - -When deploying features that include database migrations: - -### Pre-Deployment Checklist -- [ ] Identify all migrations in PR - - SQL migrations: `backend/migrations/*.sql` - - Node.js scripts: `backend/scripts/*.js` -- [ ] Review migration order and dependencies -- [ ] Check for data migration scripts -- [ ] Verify rollback procedure - -### Deployment Steps - -```bash -# 1. SSH to production -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# 2. Backup database (CRITICAL for migrations) -mkdir -p /root/backups -BACKUP_FILE="/root/backups/rotv_$(date +%Y%m%d_%H%M%S).sql" -podman exec rootsofthevalley.org pg_dump -U postgres rotv > $BACKUP_FILE -ls -lh $BACKUP_FILE # Verify backup created - -# 3. Apply SQL migrations (in order) -# Example: For PR #182 -podman exec rootsofthevalley.org psql -U postgres -d rotv \ - -f /app/migrations/015_add_poi_media.sql - -podman exec rootsofthevalley.org psql -U postgres -d rotv \ - -f /app/migrations/016_fix_poi_media_constraints.sql - -# 4. Run data migration scripts -# Example: For PR #182 -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js - -# 5. Verify migrations applied -bash scripts/verify-migrations.sh - -# 6. Pull latest image -podman pull quay.io/crunchtools/rotv:latest - -# 7. Restart service -systemctl restart rootsofthevalley.org -sleep 10 - -# 8. Verify deployment -bash scripts/post-deployment-report.sh -``` - -### Migration-Specific Verification - -```bash -# Verify table counts match expectations -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT - 'pois' as table_name, COUNT(*)::text FROM pois UNION ALL - SELECT 'poi_media', COUNT(*)::text FROM poi_media UNION ALL - SELECT 'users', COUNT(*)::text FROM users; -" - -# Check for migration-specific data -# Example: PR #182 - verify primary images migrated -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT COUNT(*) as primary_images -FROM poi_media -WHERE role='primary' AND moderation_status IN ('published', 'auto_approved'); -" -``` - ---- - -## Rollback Procedures - -### Quick Rollback (Container Only) - -Use when new container has issues but database is fine: - -```bash -# Find previous image -podman images quay.io/crunchtools/rotv - -# Tag previous image as latest -podman tag quay.io/crunchtools/rotv: quay.io/crunchtools/rotv:latest - -# Restart -systemctl restart rootsofthevalley.org - -# Verify -curl -sf https://rootsofthevalley.org/api/health -``` - -### Full Rollback (Container + Database) - -Use when database migration failed or caused issues: - -```bash -# Find backup -ls -lht /root/backups/rotv_* | head -5 - -# Restore database -BACKUP_FILE="/root/backups/rotv_TIMESTAMP.sql" -podman exec -i rootsofthevalley.org psql -U postgres rotv < $BACKUP_FILE - -# Revert container -podman tag quay.io/crunchtools/rotv: quay.io/crunchtools/rotv:latest - -# Restart -systemctl restart rootsofthevalley.org - -# Verify rollback -curl -sf https://rootsofthevalley.org/api/health -systemctl status rootsofthevalley.org -``` - ---- - -## Troubleshooting Deployments - -### Service Won't Start - -```bash -# Check service status -systemctl status rootsofthevalley.org --no-pager -l - -# Check recent logs -journalctl -u rootsofthevalley.org --since "5 minutes ago" --no-pager - -# Check container logs -podman logs rootsofthevalley.org --tail 50 - -# Common issues: -# - Port already in use: Check with `ss -tlnp | grep :3000` -# - Database not ready: Check `podman exec rootsofthevalley.org systemctl status postgresql` -# - Migration failed: Check logs for SQL errors -``` - -### Images Not Loading (PR #182 Specific) - -```bash -# Quick diagnosis -bash scripts/diagnose-production.sh - -# Check if migration script was run -podman exec rootsofthevalley.org psql -U postgres -d rotv -c \ - "SELECT COUNT(*) FROM poi_media WHERE role='primary';" -# Should return > 0 - -# If 0, run migration script -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js -systemctl restart rootsofthevalley.org -``` - -See **[PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md)** for comprehensive troubleshooting. - ---- - -## Monitoring After Deployment - -### First Hour (Critical) - -```bash -# Watch logs in real-time -journalctl -u rootsofthevalley.org -f - -# Check error count every 10 minutes -journalctl -u rootsofthevalley.org --since "10 minutes ago" | grep -i error | wc -l - -# Test key endpoints -curl -sf https://rootsofthevalley.org/api/health -curl -sf https://rootsofthevalley.org/api/pois/1/media | jq '.total_count' -``` - -### First 24 Hours - -```bash -# Check every 4 hours -bash scripts/post-deployment-report.sh - -# Look for patterns in logs -journalctl -u rootsofthevalley.org --since "4 hours ago" | grep -i error | sort | uniq -c | sort -rn -``` - ---- - -## Deployment Checklist - -Use this checklist for every deployment: - -### Pre-Deployment -- [ ] PR reviewed and approved -- [ ] All CI/CD checks passing (build, tests, security) -- [ ] Deployment runbook reviewed (if feature has one) -- [ ] Backup strategy confirmed -- [ ] Rollback procedure understood -- [ ] Estimated downtime communicated (if any) - -### During Deployment -- [ ] Database backup created and verified -- [ ] All SQL migrations applied in order -- [ ] All data migration scripts executed -- [ ] Migration verification passed -- [ ] Latest container image pulled -- [ ] Service restarted successfully -- [ ] Service active and running - -### Post-Deployment -- [ ] Health endpoint responding -- [ ] Key API endpoints tested -- [ ] Feature-specific tests passed -- [ ] Post-deployment report generated -- [ ] No critical errors in logs -- [ ] Smoke tests passed (via GitHub Actions) -- [ ] Monitoring in place for next 24 hours - -See **[DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md)** for detailed checklist. - ---- - -## Automation Scripts - -| Script | Purpose | When to Use | -|--------|---------|-------------| -| `scripts/diagnose-production.sh` | Automated health check | Before and after deployment | -| `scripts/fix-production.sh` | Automated fix for common issues | When diagnosis finds issues | -| `scripts/verify-migrations.sh` | Verify all migrations applied | After migration deployment | -| `scripts/post-deployment-report.sh` | Generate deployment report | After every deployment | - -### Running Scripts - -```bash -# All scripts should be run on production server -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# Make scripts executable (if needed) -chmod +x scripts/*.sh - -# Run diagnostic -bash scripts/diagnose-production.sh - -# Run migration verification -bash scripts/verify-migrations.sh - -# Generate post-deployment report -bash scripts/post-deployment-report.sh -``` - ---- - -## GitHub Actions Workflows - -### Smoke Tests (Manual Trigger) - -```bash -# Trigger smoke tests from local machine -gh workflow run smoke-test.yml - -# Monitor workflow -gh run watch - -# View results -gh run view -``` - -### Build Status - -```bash -# Check recent builds -gh run list --workflow=build.yml --limit 5 - -# View specific build -gh run view - -# Re-run failed build -gh run rerun -``` - ---- - -## Common Deployment Scenarios - -### Scenario 1: Simple Code Change (No Migrations) - -```bash -# 1. Wait for GHA build -gh run watch - -# 2. Deploy -ssh -p 22422 root@lotor.dc3.crunchtools.com -podman pull quay.io/crunchtools/rotv:latest -systemctl restart rootsofthevalley.org - -# 3. Verify -curl -sf https://rootsofthevalley.org/api/health && echo "✅ OK" -``` - -**Duration:** 2-3 minutes - -### Scenario 2: Database Migration (e.g., PR #182) - -```bash -# 1. Backup -ssh -p 22422 root@lotor.dc3.crunchtools.com -podman exec rootsofthevalley.org pg_dump -U postgres rotv > /root/backups/rotv_$(date +%Y%m%d_%H%M%S).sql - -# 2. Apply migrations -podman exec rootsofthevalley.org psql -U postgres -d rotv -f /app/migrations/015_add_poi_media.sql -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js -podman exec rootsofthevalley.org psql -U postgres -d rotv -f /app/migrations/016_fix_poi_media_constraints.sql - -# 3. Verify migrations -bash scripts/verify-migrations.sh - -# 4. Deploy -podman pull quay.io/crunchtools/rotv:latest -systemctl restart rootsofthevalley.org - -# 5. Verify deployment -bash scripts/post-deployment-report.sh -``` - -**Duration:** 10-15 minutes - -### Scenario 3: Emergency Rollback - -```bash -# 1. Identify issue -journalctl -u rootsofthevalley.org --since "10 minutes ago" | grep -i error - -# 2. Rollback database (if needed) -podman exec -i rootsofthevalley.org psql -U postgres rotv < /root/backups/rotv_LATEST.sql - -# 3. Rollback container -podman images quay.io/crunchtools/rotv -podman tag quay.io/crunchtools/rotv: quay.io/crunchtools/rotv:latest - -# 4. Restart -systemctl restart rootsofthevalley.org - -# 5. Verify -curl -sf https://rootsofthevalley.org/api/health -``` - -**Duration:** 3-5 minutes - ---- - -## Reference Documents - -| Document | Purpose | -|----------|---------| -| **[DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md)** | Detailed post-deployment checklist | -| **[PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md)** | Comprehensive troubleshooting guide | -| **[PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md)** | Quick reference for common fixes | -| **[PROD_ISSUE_FLOWCHART.md](./PROD_ISSUE_FLOWCHART.md)** | Visual diagrams for debugging | -| **[PRODUCTION_INCIDENT_README.md](./PRODUCTION_INCIDENT_README.md)** | Incident response guide | -| **[EXEC_SUMMARY.md](./EXEC_SUMMARY.md)** | Executive summary template | - ---- - -## Support & Escalation - -### For Deployment Issues -1. Check **[PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md)** -2. Run `scripts/diagnose-production.sh` -3. Review deployment logs -4. Consider rollback if critical - -### For Production Incidents -1. Follow **[PRODUCTION_INCIDENT_README.md](./PRODUCTION_INCIDENT_README.md)** -2. Generate incident report -3. Document resolution -4. Create prevention measures - -### Contact -- **GitHub Issues:** https://github.com/crunchtools/rotv/issues -- **Deployment Owner:** Scott McCarty (@fatherlinux) - ---- - -**Last Updated:** 2026-04-04 -**Version:** 1.0 (based on learnings from PR #182 deployment) diff --git a/DEPLOYMENT_VERIFICATION_CHECKLIST.md b/DEPLOYMENT_VERIFICATION_CHECKLIST.md deleted file mode 100644 index 98a46766..00000000 --- a/DEPLOYMENT_VERIFICATION_CHECKLIST.md +++ /dev/null @@ -1,381 +0,0 @@ -# Deployment Verification Checklist - -**Purpose:** Run this checklist after EVERY production deployment to catch issues before they impact users. - -**Time Required:** 3-5 minutes - -**When to Run:** Immediately after `systemctl restart rootsofthevalley.org` - ---- - -## Pre-Deployment Checklist - -- [ ] PR merged to master -- [ ] GitHub Actions build completed successfully -- [ ] All tests passing (including integration tests) -- [ ] No security scan failures -- [ ] Database backup created -- [ ] All required migrations identified and ready -- [ ] Deployment runbook reviewed - ---- - -## Deployment Steps - -### 1. Database Migrations ✅ - -- [ ] All SQL migrations applied (check `backend/migrations/` directory) -- [ ] All Node.js migration scripts run (check `backend/scripts/` directory) -- [ ] Migration logs reviewed for errors -- [ ] Table counts verified (if applicable) - -**Commands:** -```bash -# Check which migrations exist -ls -1 backend/migrations/*.sql | tail -5 - -# Verify each migration was applied -# (Check timestamps, no errors in output) - -# For PR #182 specifically: -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "\d poi_media" -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "SELECT COUNT(*) FROM poi_media;" -``` - -### 2. Container Deployment ✅ - -- [ ] Latest image pulled from registry -- [ ] Image tag/SHA matches expected version -- [ ] Service restarted successfully -- [ ] Container running without immediate crashes - -**Commands:** -```bash -# Pull latest -podman pull quay.io/crunchtools/rotv:latest - -# Check image timestamp -podman images quay.io/crunchtools/rotv --format "{{.CreatedAt}}" - -# Restart service -systemctl restart rootsofthevalley.org -sleep 10 - -# Verify running -systemctl status rootsofthevalley.org --no-pager -``` - -### 3. Service Health ✅ - -- [ ] Service is active and running -- [ ] No errors in startup logs -- [ ] Process listening on expected port -- [ ] Container has been up for at least 30 seconds - -**Commands:** -```bash -# Check service status -systemctl is-active rootsofthevalley.org - -# Check recent logs -journalctl -u rootsofthevalley.org --since "1 minute ago" --no-pager | tail -30 - -# Check for errors -journalctl -u rootsofthevalley.org --since "1 minute ago" --no-pager | grep -i error - -# Verify port -ss -tlnp | grep :3000 -``` - ---- - -## Post-Deployment Verification - -### 4. Database Health ✅ - -- [ ] Database connection established -- [ ] All tables exist -- [ ] Expected record counts correct -- [ ] Recent migrations reflected in schema - -**Commands:** -```bash -# Test database connection -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "SELECT version();" - -# Check table count -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'public';" - -# Check specific critical tables (adjust for your deployment) -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT - 'pois' as table_name, COUNT(*)::text as count FROM pois UNION ALL - SELECT 'poi_media', COUNT(*)::text FROM poi_media UNION ALL - SELECT 'poi_news', COUNT(*)::text FROM poi_news UNION ALL - SELECT 'users', COUNT(*)::text FROM users; -" -``` - -### 5. API Endpoints ✅ - -- [ ] Health endpoint responding -- [ ] Public API endpoints working -- [ ] Authentication endpoints working -- [ ] Admin API endpoints working (if applicable) - -**Commands:** -```bash -# Health check -curl -sf https://rootsofthevalley.org/api/health || echo "FAILED" - -# Test public API -curl -sf https://rootsofthevalley.org/api/pois?limit=1 | jq '.[0].id' || echo "FAILED" - -# Test media endpoint (PR #182 specific) -curl -sf https://rootsofthevalley.org/api/pois/1/media | jq '.total_count' || echo "FAILED" - -# Test thumbnail endpoint -curl -I https://rootsofthevalley.org/api/pois/1/thumbnail 2>&1 | grep "HTTP" || echo "FAILED" - -# Test auth status -curl -sf https://rootsofthevalley.org/api/auth/status | jq '.authenticated' || echo "FAILED" -``` - -### 6. Frontend Functionality ✅ - -- [ ] Website loads without errors -- [ ] Map displays correctly -- [ ] POI markers visible -- [ ] Sidebar opens when clicking markers -- [ ] Images load correctly -- [ ] No console errors in browser DevTools - -**Manual Steps:** -1. Open https://rootsofthevalley.org in browser -2. Open DevTools (F12) → Console tab -3. Verify map loads and displays POIs -4. Click a POI marker -5. Verify sidebar opens with POI details -6. Verify images display (no "Failed to load image" errors) -7. Check console for JavaScript errors -8. Check Network tab for failed requests (red lines) - -### 7. Feature-Specific Tests ✅ - -**For PR #182 (Multi-Image POI):** - -- [ ] Mosaic displays for POIs with multiple images -- [ ] Single image displays for POIs with one image -- [ ] Default thumbnail for POIs with no images -- [ ] Lightbox opens when clicking mosaic -- [ ] Keyboard navigation works in lightbox (arrows, ESC) -- [ ] Upload modal opens for authenticated users -- [ ] Admin can see moderation queue - -**Commands:** -```bash -# Check media counts -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT - media_type, - role, - COUNT(*) -FROM poi_media -GROUP BY media_type, role; -" - -# Check for any pending moderation items -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT COUNT(*) FROM moderation_queue WHERE content_type = 'photo'; -" -``` - -### 8. Performance & Resources ✅ - -- [ ] Response times acceptable (<1s for most endpoints) -- [ ] Memory usage normal -- [ ] CPU usage normal -- [ ] No resource exhaustion warnings - -**Commands:** -```bash -# Check response time -time curl -s https://rootsofthevalley.org/api/pois/1/media > /dev/null - -# Check container resource usage -podman stats --no-stream rootsofthevalley.org - -# Check system resources -free -h -df -h -``` - -### 9. Error Rates ✅ - -- [ ] No spike in 500 errors -- [ ] No spike in 404 errors -- [ ] No database connection errors -- [ ] No authentication failures - -**Commands:** -```bash -# Check for errors in last 5 minutes -journalctl -u rootsofthevalley.org --since "5 minutes ago" --no-pager | grep -c "error" - -# Check for specific error types -journalctl -u rootsofthevalley.org --since "5 minutes ago" --no-pager | grep -E "500|404|ECONNREFUSED|ETIMEDOUT" | wc -l - -# Sample recent logs -journalctl -u rootsofthevalley.org --since "5 minutes ago" --no-pager | tail -50 -``` - -### 10. External Dependencies ✅ - -- [ ] Image server connectivity verified -- [ ] Database server connectivity verified -- [ ] Any third-party APIs responding -- [ ] OAuth providers working (if applicable) - -**Commands:** -```bash -# Check image server -podman exec rootsofthevalley.org curl -sf http://10.89.1.100:8000/api/health || echo "FAILED" - -# Check IMAGE_SERVER_URL env var -podman exec rootsofthevalley.org printenv IMAGE_SERVER_URL - -# Test asset fetch -curl -I https://rootsofthevalley.org/api/assets/test-id/thumbnail 2>&1 | grep "HTTP" -# Should return 400 (bad request) which proves validation is working -``` - ---- - -## Rollback Decision Matrix - -| Symptom | Severity | Rollback? | -|---------|----------|-----------| -| Service won't start | 🔴 Critical | **YES** - Immediate rollback | -| Database migration failed | 🔴 Critical | **YES** - Restore backup | -| 500 errors on all endpoints | 🔴 Critical | **YES** - Immediate rollback | -| Images not loading | 🟡 Major | **NO** - Fix forward (run migration script) | -| Single feature broken | 🟡 Major | **MAYBE** - Evaluate impact | -| Minor UI glitch | 🟢 Minor | **NO** - Fix forward | -| Performance degradation | 🟡 Major | **MAYBE** - Monitor and decide | - ---- - -## Rollback Procedure (If Needed) - -### Quick Rollback (Container Only) -```bash -# Find previous working image -podman images quay.io/crunchtools/rotv - -# Tag previous image as latest -podman tag quay.io/crunchtools/rotv: quay.io/crunchtools/rotv:latest - -# Restart -systemctl restart rootsofthevalley.org -``` - -### Full Rollback (Container + Database) -```bash -# Find backup -ls -lht /root/backups/rotv_* | head -5 - -# Restore database -podman exec -i rootsofthevalley.org psql -U postgres rotv < /root/backups/rotv_TIMESTAMP.sql - -# Revert container (same as above) -podman tag quay.io/crunchtools/rotv: quay.io/crunchtools/rotv:latest -systemctl restart rootsofthevalley.org - -# Verify rollback worked -curl -sf https://rootsofthevalley.org/api/health -``` - ---- - -## Monitoring (First 24 Hours) - -### Hour 1 (Critical) -- [ ] Check logs every 10 minutes -- [ ] Monitor error rates -- [ ] Watch for user reports - -### Hours 2-6 (Important) -- [ ] Check logs every hour -- [ ] Review error patterns -- [ ] Test key workflows manually - -### Hours 7-24 (Normal) -- [ ] Check logs every 4 hours -- [ ] Review metrics/stats -- [ ] Note any anomalies - -**Commands:** -```bash -# Watch logs live -journalctl -u rootsofthevalley.org -f - -# Check error rate (run periodically) -journalctl -u rootsofthevalley.org --since "1 hour ago" --no-pager | grep -i error | wc -l - -# Check for specific issues -journalctl -u rootsofthevalley.org --since "1 hour ago" --no-pager | grep -i "failed to\|error\|exception" -``` - ---- - -## Sign-Off - -**Deployment Date:** ________________ -**Deployed By:** ________________ -**PR/Version:** ________________ - -**All checks passed:** ☐ YES ☐ NO -**Issues found:** ________________ -**Issues resolved:** ☐ YES ☐ NO ☐ N/A -**Rollback performed:** ☐ YES ☐ NO - -**Notes:** -``` -_________________________________________________________________________ -_________________________________________________________________________ -_________________________________________________________________________ -``` - ---- - -## Automation Ideas (Future) - -1. **Smoke Test Script** - - Runs all verification commands automatically - - Exits with error code if any check fails - - Can be triggered by CI/CD or manually - -2. **Health Dashboard** - - Real-time status of all checks - - Historical metrics - - Alert on anomalies - -3. **Automated Rollback** - - Detect critical failures automatically - - Trigger rollback without human intervention - - Send alerts to ops team - -4. **Canary Deployments** - - Deploy to subset of users first - - Monitor metrics before full rollout - - Auto-rollback if issues detected - ---- - -## Resources - -- **Deployment Runbook:** `.specify/specs/004-multi-image-poi/DEPLOYMENT_RUNBOOK.md` -- **Troubleshooting Guide:** `PROD_TROUBLESHOOT.md` -- **Fix Quick Reference:** `PROD_FIX_QUICKREF.md` -- **Diagnostic Script:** `scripts/diagnose-production.sh` -- **Fix Script:** `scripts/fix-production.sh` diff --git a/EXEC_SUMMARY.md b/EXEC_SUMMARY.md deleted file mode 100644 index 75e5b881..00000000 --- a/EXEC_SUMMARY.md +++ /dev/null @@ -1,249 +0,0 @@ -# Executive Summary: Production Image Loading Issue (PR #182) - -**Date:** 2026-04-04 -**Status:** 🔴 BROKEN - Images not loading in production -**Impact:** All POI images failing to load with "Failed to load image" error -**Time to Fix:** ~5 minutes -**Difficulty:** Low (single script execution) - ---- - -## Problem - -PR #182 (Multi-Image POI Support) changed how images are served from direct image server queries to database-backed queries using a new `poi_media` table. The database migration created the table structure, but **the script to populate the table with existing images was not run during deployment**. - -### User Impact -- ❌ All POI images show "Failed to load image" error -- ❌ Image thumbnails return 404 -- ❌ Mosaic display shows nothing -- ✅ Map and text content work fine -- ✅ No data loss (images exist on image server) - ---- - -## Root Cause - -``` -New Code Path: - /api/pois/:id/thumbnail - → SELECT FROM poi_media WHERE role='primary' ← Empty table! - → Returns 404 "Image not found" - -Missing Step: - migrate-primary-images.js script not run - → Table structure exists (migration 015 ✅) - → Table has ZERO records ❌ - → Expected: ~75 records -``` - ---- - -## The Fix (Copy-Paste Solution) - -### Option 1: Automated Fix Script (Recommended) - -```bash -# SSH to production -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# Run fix script (interactive, creates backup, safe) -cd /path/to/rotv # wherever repo is checked out -bash scripts/fix-production.sh -``` - -**Duration:** 2-3 minutes (includes backup, migration, restart, verification) - -### Option 2: Manual Fix (If Automated Script Unavailable) - -```bash -# SSH to production -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# 1. Backup (30 seconds) -mkdir -p /root/backups -podman exec rootsofthevalley.org pg_dump -U postgres rotv > /root/backups/rotv_backup_$(date +%Y%m%d_%H%M%S).sql - -# 2. Run migration script (1-2 minutes) -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js - -# 3. Apply data integrity migration (10 seconds) -podman exec rootsofthevalley.org psql -U postgres -d rotv -f /app/migrations/016_fix_poi_media_constraints.sql - -# 4. Restart service (30 seconds) -systemctl restart rootsofthevalley.org -sleep 10 - -# 5. Verify (10 seconds) -curl -s https://rootsofthevalley.org/api/pois/1/media | jq '.total_count' -# Should return a number > 0 -``` - -**Duration:** 3-4 minutes total - ---- - -## Verification - -### Quick Check (30 seconds) -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com \ - "podman exec rootsofthevalley.org psql -U postgres -d rotv -tAc 'SELECT COUNT(*) FROM poi_media WHERE role='\''primary'\'';'" - -# Current (broken): 0 -# After fix: 75 (or similar number) -``` - -### Full Verification (2 minutes) -1. Open https://rootsofthevalley.org -2. Click any POI marker -3. Images should display in sidebar -4. No errors in browser console - ---- - -## What Happened - -### Deployment Checklist -- ✅ PR #182 merged -- ✅ Container built and published -- ✅ Database backed up -- ✅ Migration 015 applied (creates `poi_media` table) -- ❌ **migrate-primary-images.js script NOT run** ← ROOT CAUSE -- ❌ Migration 016 may not have been applied -- ✅ New container deployed -- ✅ Service restarted -- ❌ Verification not performed (would have caught this) - -### Why It Was Missed -- Deployment runbook steps 5-6 were skipped -- No automated post-deployment verification -- Dev/CI environments work (ephemeral databases always run full migration) -- Production requires manual data migration step - ---- - -## Risk Assessment - -| Aspect | Risk Level | Notes | -|--------|-----------|-------| -| **Data Loss** | 🟢 None | Images exist on image server, just not indexed in database | -| **Service Downtime** | 🟡 Low | Fix requires service restart (~30s downtime) | -| **Rollback Complexity** | 🟢 Simple | Restore database backup if needed | -| **User Data Impact** | 🟢 None | Read-only operation, no user data affected | -| **Fix Complexity** | 🟢 Trivial | Single script execution | - ---- - -## Technical Details - -### What the Migration Script Does -1. Queries `pois` table for all records with `has_primary_image = true` -2. For each POI, fetches primary asset from image server -3. Creates `poi_media` record with: - - `poi_id` (foreign key to pois) - - `media_type = 'image'` - - `image_server_asset_id` (from image server) - - `role = 'primary'` - - `moderation_status = 'published'` -4. Skips POIs that already have primary entries (idempotent) - -### Dependencies -- Image server must be reachable at `http://10.89.1.100:8000` -- `IMAGE_SERVER_URL` environment variable must be set -- Container must have network access to image server - -### Database Changes -```sql --- Before (broken) -SELECT COUNT(*) FROM poi_media WHERE role='primary'; --- Result: 0 - --- After (fixed) -SELECT COUNT(*) FROM poi_media WHERE role='primary'; --- Result: 75 (number of POIs with images) -``` - ---- - -## Communication - -### Status Update Template - -**For Stakeholders:** -> Production image loading issue identified in PR #182 deployment. Root cause: database migration script was not executed. Fix is straightforward (5 minute script execution). No data loss, no user data impacted. ETA to resolution: 10 minutes. - -**For Users (if needed):** -> We're aware that images are not loading on Roots of The Valley. Our team is working on a fix and expects to have this resolved within 10 minutes. Your data is safe and no information has been lost. Thank you for your patience. - ---- - -## Prevention for Next Time - -### Immediate (Next Deployment) -1. Add verification step to deployment runbook -2. Run `scripts/diagnose-production.sh` after every deployment -3. Check key metrics (table counts, API endpoints) - -### Short Term (Next Sprint) -1. Add smoke test script to GitHub Actions -2. Create post-deployment checklist -3. Document all manual migration scripts in runbook - -### Long Term (Future) -1. Automate database migrations in systemd service -2. Add health check endpoint that verifies table counts -3. Create monitoring alerts for 404 rates on image endpoints - ---- - -## Resources - -| Document | Purpose | -|----------|---------| -| `PROD_FIX_QUICKREF.md` | Quick reference commands | -| `PROD_TROUBLESHOOT.md` | Comprehensive troubleshooting guide | -| `PROD_ISSUE_FLOWCHART.md` | Visual diagrams and data flow | -| `scripts/diagnose-production.sh` | Automated diagnostics | -| `scripts/fix-production.sh` | Automated fix with backup | -| `.specify/specs/004-multi-image-poi/DEPLOYMENT_RUNBOOK.md` | Full deployment procedure | - ---- - -## Questions & Answers - -**Q: Is data lost?** -A: No. Images exist on the image server. The database just doesn't have references to them yet. - -**Q: Will this affect other services?** -A: No. This only affects image loading on rootsofthevalley.org. - -**Q: Can we roll back?** -A: Yes. Database backup will be created before fix. Can restore in under 1 minute if needed. - -**Q: How long will the fix take?** -A: 3-5 minutes total (backup, migration, restart, verification). - -**Q: What if the fix fails?** -A: Rollback procedure is simple: restore database backup and restart service. No lasting impact. - -**Q: Why didn't testing catch this?** -A: Dev/CI use ephemeral databases that always run full migrations. Production has existing data requiring manual migration. - -**Q: Will this happen again?** -A: Adding verification steps and automated smoke tests to prevent recurrence. - ---- - -## Next Steps - -1. **Immediate:** Run fix script (see "The Fix" section above) -2. **Verify:** Confirm images load in browser -3. **Monitor:** Watch logs for 24 hours for any related issues -4. **Document:** Update deployment runbook with verification steps -5. **Prevent:** Add smoke tests to CI/CD pipeline - ---- - -**Contact:** Scott McCarty (@fatherlinux) -**PR #182:** https://github.com/crunchtools/rotv/pull/182 -**Server:** lotor.dc3.crunchtools.com:22422 diff --git a/HANDOFF_SUMMARY.md b/HANDOFF_SUMMARY.md deleted file mode 100644 index 2583c2bc..00000000 --- a/HANDOFF_SUMMARY.md +++ /dev/null @@ -1,424 +0,0 @@ -# Production Troubleshooting Package - Handoff Summary - -**Date:** 2026-04-04 -**Created By:** Claude Sonnet 4.5 -**Purpose:** Response to PR #182 production image loading failure -**Status:** ✅ COMPLETE - Ready for immediate use - ---- - -## 🎯 Executive Summary - -I've created a comprehensive troubleshooting package in response to the production image loading issue (PR #182). The package includes 16 files with complete diagnostics, automated fixes, deployment guides, and prevention measures. - -**The Problem:** Images not loading on rootsofthevalley.org after PR #182 deployment -**Root Cause:** Database migration script (`migrate-primary-images.js`) not executed -**Impact:** All POI images showing "Failed to load image" error -**Fix Time:** 5 minutes -**Fix Difficulty:** Low (single script execution) - ---- - -## 📦 What Was Created - -### Documentation (11 files, ~4,500 lines) - -**Entry Points:** -- `README_PRODUCTION.md` - Main production operations guide -- `PRODUCTION_INCIDENT_README.md` - Incident response (START HERE if issue active) -- `TROUBLESHOOTING_PACKAGE_INDEX.md` - Complete package index -- `NEXT_STEPS.md` - Action plan and timelines -- `PACKAGE_STRUCTURE.md` - Visual guide to all resources - -**Operational Guides:** -- `DEPLOYMENT_GUIDE.md` - Complete deployment procedures -- `DEPLOYMENT_VERIFICATION_CHECKLIST.md` - Post-deployment verification -- `PROD_TROUBLESHOOT.md` - Comprehensive troubleshooting (diagnostic steps, fixes, common errors) -- `PROD_FIX_QUICKREF.md` - Quick reference with copy-paste commands -- `PROD_ISSUE_FLOWCHART.md` - Visual diagrams (data flow, debugging) -- `EXEC_SUMMARY.md` - Executive summary template for stakeholders - -### Scripts (4 files, ~1,200 lines) - -All scripts are executable and production-ready: -- `scripts/diagnose-production.sh` - Automated diagnostics (30+ health checks in 30 seconds) -- `scripts/fix-production.sh` - Automated fix with backup (interactive, safe) -- `scripts/verify-migrations.sh` - Migration verification (schema, indexes, constraints, data) -- `scripts/post-deployment-report.sh` - Deployment health report (Markdown output) - -### Automation (1 file, 234 lines) - -- `.github/workflows/smoke-test.yml` - Post-deployment smoke tests (9 tests, GitHub Actions) - ---- - -## 🚀 Immediate Action Required - -### The Fix (5 minutes) - -```bash -# SSH to production -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# Run migration script (populates poi_media table) -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js - -# Apply data integrity migration -podman exec rootsofthevalley.org psql -U postgres -d rotv \ - -f /app/migrations/016_fix_poi_media_constraints.sql - -# Restart service -systemctl restart rootsofthevalley.org && sleep 10 - -# Verify -curl -s https://rootsofthevalley.org/api/pois/1/media | jq '.total_count' -# Should return > 0 -``` - -### Verification (1 minute) - -```bash -# Test in browser -open https://rootsofthevalley.org -# Click any POI → Images should load - -# Generate report -bash scripts/post-deployment-report.sh -``` - -**See:** `PROD_FIX_QUICKREF.md` for detailed commands - ---- - -## 📚 How to Use This Package - -### Three Resolution Paths - -**Path 1: Just Fix It (5 minutes)** -- For: Experienced ops, need immediate resolution -- Read: `PROD_FIX_QUICKREF.md` -- Execute: Copy-paste commands -- Time: 5 minutes - -**Path 2: Diagnose First (10 minutes)** -- For: Methodical troubleshooting -- Run: `scripts/diagnose-production.sh` -- Review: Output and recommendations -- Execute: `scripts/fix-production.sh` (if issues found) -- Time: 10 minutes - -**Path 3: Understand First (30 minutes)** -- For: Learning, prevention, root cause analysis -- Read: `EXEC_SUMMARY.md` → `PROD_ISSUE_FLOWCHART.md` → `DEPLOYMENT_GUIDE.md` -- Understand: Why it happened, how to prevent -- Execute: Fix from Path 1 or 2 -- Time: 30 minutes - -### By Role - -**On-Call Engineer:** -1. `README_PRODUCTION.md` (5 min) -2. `PRODUCTION_INCIDENT_README.md` (choose path) -3. Apply fix -4. Monitor - -**DevOps Engineer:** -1. `DEPLOYMENT_GUIDE.md` (15 min) -2. `DEPLOYMENT_VERIFICATION_CHECKLIST.md` (reference) -3. Run `scripts/post-deployment-report.sh` after deployments -4. Setup `smoke-test.yml` automation - -**Support Engineer:** -1. `README_PRODUCTION.md` (5 min) -2. `PROD_TROUBLESHOOT.md` (reference) -3. Run `scripts/diagnose-production.sh` for issues -4. Use `PROD_FIX_QUICKREF.md` for common fixes - ---- - -## 🎯 Key Features - -### Automation -✅ **30+ automated health checks** - `scripts/diagnose-production.sh` -✅ **Automated fix with backup** - `scripts/fix-production.sh` -✅ **Migration verification** - `scripts/verify-migrations.sh` -✅ **Deployment reporting** - `scripts/post-deployment-report.sh` -✅ **Smoke tests** - `.github/workflows/smoke-test.yml` - -### Documentation -✅ **Multiple reading paths** - Fast (5min), Medium (10min), Thorough (30min) -✅ **Visual diagrams** - Flowcharts, data flow, architecture -✅ **50+ copy-paste commands** - No guessing, ready to use -✅ **Complete troubleshooting** - Diagnosis → Fix → Verify → Prevent -✅ **Stakeholder communication** - Executive summary templates - -### Prevention -✅ **Deployment checklists** - Prevent skipped steps -✅ **Post-deployment verification** - Catch issues before users -✅ **Automated smoke tests** - CI/CD integration -✅ **Lessons learned** - Documented for future -✅ **Rollback procedures** - Safe, tested recovery - ---- - -## 📊 Package Statistics - -| Metric | Value | -|--------|-------| -| **Total Files** | 16 | -| **Documentation Files** | 11 | -| **Script Files** | 4 | -| **Workflow Files** | 1 | -| **Documentation Lines** | ~4,500 | -| **Script Lines** | ~1,200 | -| **Diagnostic Checks** | 30+ | -| **Copy-Paste Commands** | 50+ | -| **Coverage** | Complete incident lifecycle | -| **Time to Fix** | 5 minutes | -| **Time to Diagnose** | 30 seconds (automated) | - ---- - -## 🔄 Next Steps Timeline - -### Immediate (Today) -- [ ] Apply fix to production (5 minutes) -- [ ] Verify images loading (1 minute) -- [ ] Generate post-deployment report (30 seconds) -- [ ] Begin 24-hour monitoring - -### Short-Term (This Week) -- [ ] Monitor production for 24 hours -- [ ] Document incident using `EXEC_SUMMARY.md` -- [ ] Run smoke tests: `gh workflow run smoke-test.yml` -- [ ] Review all new documentation with team - -### Medium-Term (This Month) -- [ ] Integrate smoke tests into CI/CD -- [ ] Set up monitoring alerts -- [ ] Train team on new procedures -- [ ] Improve deployment automation - -### Long-Term (This Quarter) -- [ ] Automated deployment pipeline -- [ ] Proactive monitoring -- [ ] Regular incident drills -- [ ] Continuous improvement - -**See:** `NEXT_STEPS.md` for detailed timelines and tasks - ---- - -## 📖 Document Roadmap - -### Start Here (Entry Points) -``` -README_PRODUCTION.md - ↓ -├─ Active Incident? → PRODUCTION_INCIDENT_README.md -├─ Deploying? → DEPLOYMENT_GUIDE.md -├─ Troubleshooting? → PROD_TROUBLESHOOT.md -└─ Overview? → TROUBLESHOOTING_PACKAGE_INDEX.md -``` - -### Incident Response Flow -``` -PRODUCTION_INCIDENT_README.md (Choose path) - ↓ -├─ Fast Fix → PROD_FIX_QUICKREF.md -├─ Diagnose → scripts/diagnose-production.sh -└─ Understand → EXEC_SUMMARY.md + PROD_ISSUE_FLOWCHART.md - ↓ -Apply Fix - ↓ -Verify (scripts/post-deployment-report.sh) - ↓ -Monitor - ↓ -Document (EXEC_SUMMARY.md template) -``` - -### Deployment Flow -``` -DEPLOYMENT_GUIDE.md - ↓ -Deploy (Standard or Migration) - ↓ -DEPLOYMENT_VERIFICATION_CHECKLIST.md - ↓ -scripts/post-deployment-report.sh - ↓ -smoke-test.yml (GitHub Actions) - ↓ -Monitor -``` - -**See:** `PACKAGE_STRUCTURE.md` for complete visual guide - ---- - -## 🎓 Learning Outcomes - -After using this package, operators can: -- ✅ Diagnose production issues in < 2 minutes -- ✅ Fix common issues in < 5 minutes -- ✅ Verify deployments systematically -- ✅ Rollback safely when needed -- ✅ Communicate effectively with stakeholders -- ✅ Prevent issues through checklists -- ✅ Automate common tasks -- ✅ Document incidents properly - ---- - -## 🔍 Quality Assurance - -This package includes: -- ✅ Quick start guides (< 5 minutes to resolution) -- ✅ Comprehensive troubleshooting (all scenarios covered) -- ✅ Automated diagnostics (no manual checks needed) -- ✅ Automated fixes (safe with backups) -- ✅ Visual diagrams (data flow, flowcharts) -- ✅ Copy-paste commands (no guessing) -- ✅ Rollback procedures (tested and safe) -- ✅ Verification checklists (prevent issues) -- ✅ Incident templates (stakeholder communication) -- ✅ Prevention measures (lessons learned) -- ✅ Cross-references (easy navigation) -- ✅ Multiple reading paths (all skill levels) - ---- - -## 📞 Support & Escalation - -### Quick Reference -- **Production URL:** https://rootsofthevalley.org -- **Server:** lotor.dc3.crunchtools.com:22422 -- **Service:** rootsofthevalley.org -- **Container:** quay.io/crunchtools/rotv:latest -- **Database:** PostgreSQL 17 (rotv) - -### Resources -- **GitHub Issues:** https://github.com/crunchtools/rotv/issues -- **PR #182:** https://github.com/crunchtools/rotv/pull/182 -- **Owner:** Scott McCarty (@fatherlinux) - -### Emergency Commands - -```bash -# Service status -systemctl status rootsofthevalley.org - -# View logs -journalctl -u rootsofthevalley.org --no-pager -n 50 - -# Test health -curl -sf https://rootsofthevalley.org/api/health - -# Emergency rollback (one-liner) -podman tag quay.io/crunchtools/rotv:$(podman images quay.io/crunchtools/rotv --format "{{.Tag}}" | grep -v latest | head -1) quay.io/crunchtools/rotv:latest && systemctl restart rootsofthevalley.org -``` - ---- - -## ✅ Handoff Checklist - -### Package Completeness -- [x] All documentation created (11 files) -- [x] All scripts created (4 files) -- [x] GitHub Actions workflow created (1 file) -- [x] All scripts executable -- [x] All documentation cross-referenced -- [x] README.md updated with production operations section -- [x] Package tested and verified - -### Documentation Quality -- [x] Multiple reading paths (fast/medium/thorough) -- [x] Clear navigation between documents -- [x] Copy-paste commands provided -- [x] Visual aids included -- [x] Examples and use cases -- [x] Troubleshooting for common issues -- [x] Rollback procedures documented - -### Automation -- [x] Diagnostic script (diagnose-production.sh) -- [x] Fix script (fix-production.sh) -- [x] Migration verification (verify-migrations.sh) -- [x] Reporting script (post-deployment-report.sh) -- [x] Smoke tests (smoke-test.yml) - -### Production Ready -- [x] Fix identified and documented -- [x] Fix procedure tested -- [x] Rollback procedure documented -- [x] Verification steps clear -- [x] Monitoring guidance provided - -### Team Enablement -- [x] Multiple entry points for different roles -- [x] Clear action items (NEXT_STEPS.md) -- [x] Training recommendations -- [x] Prevention measures documented -- [x] Continuous improvement roadmap - ---- - -## 🎁 Deliverables Summary - -``` -✅ 16 files created -✅ ~5,700 lines of documentation and code -✅ 30+ automated diagnostic checks -✅ 50+ copy-paste commands -✅ 9 smoke tests -✅ Complete incident lifecycle coverage -✅ Production-ready automation -✅ Team enablement resources -``` - ---- - -## 🔐 Final Notes - -### What This Package Solves -1. **Immediate:** Fixes production image loading issue (5 minutes) -2. **Short-term:** Provides comprehensive troubleshooting tools -3. **Medium-term:** Prevents similar issues through checklists and automation -4. **Long-term:** Enables team self-sufficiency and continuous improvement - -### What Makes This Package Unique -- **Comprehensive:** Covers entire incident lifecycle (detect → diagnose → fix → verify → prevent) -- **Automated:** Scripts reduce manual work and human error -- **Accessible:** Multiple reading paths for different skill levels -- **Actionable:** Copy-paste commands, no guessing -- **Visual:** Diagrams and flowcharts for understanding -- **Preventive:** Checklists and automation to stop recurrence - -### Success Metrics -- **Time to fix:** 5 minutes (vs hours of debugging) -- **Time to diagnose:** 30 seconds automated (vs manual investigation) -- **Coverage:** Complete (all scenarios documented) -- **Usability:** High (copy-paste commands, visual aids) -- **Prevention:** Built-in (checklists, automation, monitoring) - ---- - -## 🚀 Ready to Use - -**This package is production-ready and can be used immediately.** - -1. Fix the production issue: `PROD_FIX_QUICKREF.md` -2. Learn the system: `README_PRODUCTION.md` -3. Deploy safely: `DEPLOYMENT_GUIDE.md` -4. Respond to incidents: `PRODUCTION_INCIDENT_README.md` -5. Prevent recurrence: `DEPLOYMENT_VERIFICATION_CHECKLIST.md` - ---- - -**Created:** 2026-04-04 -**Version:** 1.0 -**Status:** ✅ COMPLETE -**Handoff Complete:** Ready for production use - -**Questions?** See `TROUBLESHOOTING_PACKAGE_INDEX.md` for complete package overview diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md deleted file mode 100644 index 96faf3fe..00000000 --- a/NEXT_STEPS.md +++ /dev/null @@ -1,399 +0,0 @@ -# Next Steps - Production Image Loading Issue (PR #182) - -**Date:** 2026-04-04 -**Status:** 🔴 ACTION REQUIRED -**Priority:** HIGH (user-facing feature broken) -**Time to Fix:** 5 minutes - ---- - -## Immediate Action Required (Do This Now) - -### Step 1: Apply the Fix (5 minutes) - -```bash -# SSH to production server -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# Run the migration script to populate poi_media table -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js - -# Apply data integrity constraints (migration 016) -podman exec rootsofthevalley.org psql -U postgres -d rotv \ - -f /app/migrations/016_fix_poi_media_constraints.sql - -# Restart the service -systemctl restart rootsofthevalley.org - -# Wait for startup -sleep 10 - -# Verify service is running -systemctl status rootsofthevalley.org -``` - -### Step 2: Verify the Fix (1 minute) - -```bash -# Check database - should show primary images -podman exec rootsofthevalley.org psql -U postgres -d rotv -c \ - "SELECT COUNT(*) FROM poi_media WHERE role='primary';" -# Expected: > 0 (likely 50-100) - -# Test API endpoint -curl -s https://rootsofthevalley.org/api/pois/1/media | jq '.total_count' -# Expected: > 0 - -# Check for errors in logs -journalctl -u rootsofthevalley.org --since "1 minute ago" | grep -i error -# Expected: No critical errors -``` - -### Step 3: Test in Browser (1 minute) - -1. Open https://rootsofthevalley.org -2. Click any POI marker on the map -3. Verify images display in sidebar (no "Failed to load image" errors) -4. Open browser DevTools (F12) → Console tab -5. Verify no JavaScript errors related to images - -### Step 4: Generate Post-Deployment Report (30 seconds) - -```bash -bash scripts/post-deployment-report.sh -``` - -Review the report for any warnings or issues. - ---- - -## Short-Term Actions (This Week) - -### Monday: Monitor Production (1-2 hours spread over day) - -```bash -# Check every 2 hours for first 24 hours -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# Quick health check -systemctl status rootsofthevalley.org -curl -sf https://rootsofthevalley.org/api/health && echo "✅ OK" || echo "❌ FAILED" - -# Check error count -journalctl -u rootsofthevalley.org --since "2 hours ago" | grep -i error | wc -l - -# Check media endpoint is working -curl -s https://rootsofthevalley.org/api/pois/1/media | jq '.total_count' -``` - -**What to watch for:** -- Error rate spike (> 20 errors per hour) -- 404 errors on image requests -- Service crashes or restarts -- Slow response times (> 2 seconds) - -**If issues appear:** -- Review [PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md) -- Run `scripts/diagnose-production.sh` -- Consider rollback if critical - -### Tuesday: Document the Incident (30 minutes) - -Using the [EXEC_SUMMARY.md](./EXEC_SUMMARY.md) template, document: - -1. **What happened** - - PR #182 deployed without running migration script - - `poi_media` table empty, causing 404 on all images - - Detected: [TIME] - - Resolved: [TIME] - -2. **Impact** - - All POI images showing "Failed to load image" - - Duration: [DURATION] - - Users affected: All site visitors - - Data loss: None - -3. **Root cause** - - Deployment runbook steps 5-6 skipped - - No post-deployment verification performed - - No automated smoke tests - -4. **Resolution** - - Ran `migrate-primary-images.js` script - - Applied migration 016 constraints - - Restarted service - - Verified images loading - -5. **Prevention measures** - - Add smoke tests to CI/CD ✅ (smoke-test.yml created) - - Create deployment verification checklist ✅ (created) - - Update deployment guide with verification steps ✅ (updated) - - Create diagnostic/fix scripts ✅ (created) - -**Action:** Save completed summary to project wiki or team knowledge base - -### Wednesday: Run Smoke Tests (5 minutes) - -```bash -# Trigger automated smoke tests -gh workflow run smoke-test.yml - -# Monitor execution -gh run watch - -# Review results -gh run view -``` - -**Expected:** All tests should pass - -**If failures:** Investigate and fix before marking incident closed - ---- - -## Medium-Term Actions (This Month) - -### Week 1: Improve Deployment Process (2-3 hours) - -- [ ] **Add automated smoke tests to CI/CD** - - Modify `.github/workflows/build.yml` to trigger `smoke-test.yml` on successful build - - Require smoke tests to pass before deployment - -- [ ] **Create deployment automation** - - Build script that runs all deployment steps in order - - Include automatic backup, migration verification, smoke tests - - Add safety checks (confirm steps, rollback on failure) - -- [ ] **Update systemd service file** - - Consider adding health check monitoring - - Set up automatic restart on failure - - Configure resource limits - -**Owner:** DevOps/Platform team -**Due:** End of month - -### Week 2: Improve Monitoring (2-3 hours) - -- [ ] **Set up monitoring alerts** - - Error rate threshold alerts (> 20 errors/hour) - - Service down alerts - - API response time alerts (> 2 seconds) - - Database connection failure alerts - -- [ ] **Create dashboard** - - Service health status - - API endpoint status - - Error rates over time - - Database table counts - -- [ ] **Weekly health check schedule** - - Run `scripts/diagnose-production.sh` weekly - - Review logs for patterns - - Generate monthly reports - -**Owner:** Operations team -**Due:** 2 weeks - -### Week 3-4: Team Training (1-2 hours) - -- [ ] **Document walkthrough** - - Present troubleshooting package to team - - Walk through common scenarios - - Practice using diagnostic scripts - - Review rollback procedures - -- [ ] **On-call runbook** - - Add to on-call documentation - - Include in incident response playbook - - Test with mock incident - -- [ ] **Knowledge sharing** - - Add to team wiki - - Share in team meeting - - Create quick reference cards - -**Owner:** Team lead -**Due:** End of month - ---- - -## Long-Term Actions (This Quarter) - -### Deployment Automation (Sprint 1) - -**Goal:** Zero-touch deployments with automatic verification - -**Tasks:** -- [ ] Automated migration detection and execution -- [ ] Canary deployment support (deploy to subset first) -- [ ] Automatic rollback on smoke test failure -- [ ] Blue/green deployment capability - -**Success Metrics:** -- 100% of deployments include smoke tests -- 0% of deployments skip migration steps -- < 5 minutes average deployment time - -### Monitoring & Observability (Sprint 2) - -**Goal:** Proactive issue detection before users notice - -**Tasks:** -- [ ] Implement APM (Application Performance Monitoring) -- [ ] Set up log aggregation and search -- [ ] Create alerting rules for anomalies -- [ ] Build real-time dashboard - -**Success Metrics:** -- Issues detected before user reports -- < 5 minutes mean time to detection (MTTD) -- < 15 minutes mean time to resolution (MTTR) - -### Documentation & Training (Sprint 3) - -**Goal:** All team members can handle production issues - -**Tasks:** -- [ ] Quarterly incident response drills -- [ ] Update runbooks based on new incidents -- [ ] Create video walkthroughs -- [ ] Build self-service diagnostic tools - -**Success Metrics:** -- 100% team trained on incident response -- > 80% incidents resolved using runbooks -- < 10% repeat incidents - ---- - -## Success Criteria - -### Immediate (24 hours) -- [x] Troubleshooting package created ✅ -- [ ] Fix applied to production -- [ ] Images loading correctly -- [ ] No errors in logs -- [ ] Post-deployment report generated -- [ ] Monitoring active - -### Short-term (1 week) -- [ ] 24-hour monitoring period completed with no issues -- [ ] Incident documented -- [ ] Smoke tests passing -- [ ] Team aware of new troubleshooting resources - -### Medium-term (1 month) -- [ ] Smoke tests integrated into CI/CD -- [ ] Monitoring alerts configured -- [ ] Team trained on new procedures -- [ ] Deployment automation improved - -### Long-term (3 months) -- [ ] Zero deployment incidents -- [ ] Automated deployment pipeline -- [ ] Proactive monitoring in place -- [ ] Team fully self-sufficient - ---- - -## Metrics to Track - -### Deployment Health -- Deployments with migrations: 100% run migration scripts -- Deployments with verification: 100% run smoke tests -- Failed deployments: 0 -- Rollbacks required: 0 - -### Incident Response -- Time to detect (MTTD): < 5 minutes -- Time to diagnose: < 5 minutes -- Time to fix: < 15 minutes -- Time to verify: < 5 minutes - -### Service Health -- Uptime: > 99.9% -- Error rate: < 0.1% -- API response time: < 500ms (p95) -- Image load success rate: > 99% - ---- - -## Resources Created - -### Documentation (9 files) -- ✅ README_PRODUCTION.md - Main production guide -- ✅ DEPLOYMENT_GUIDE.md - Deployment procedures -- ✅ DEPLOYMENT_VERIFICATION_CHECKLIST.md - Post-deployment checklist -- ✅ PRODUCTION_INCIDENT_README.md - Incident response -- ✅ EXEC_SUMMARY.md - Executive summary template -- ✅ PROD_TROUBLESHOOT.md - Comprehensive troubleshooting -- ✅ PROD_FIX_QUICKREF.md - Quick reference -- ✅ PROD_ISSUE_FLOWCHART.md - Visual debugging -- ✅ TROUBLESHOOTING_PACKAGE_INDEX.md - Package index - -### Scripts (4 files) -- ✅ scripts/diagnose-production.sh - Automated diagnostics -- ✅ scripts/fix-production.sh - Automated fix -- ✅ scripts/verify-migrations.sh - Migration verification -- ✅ scripts/post-deployment-report.sh - Deployment reporting - -### Automation (1 file) -- ✅ .github/workflows/smoke-test.yml - Smoke tests - -**Total:** 14 files, ~4,000 lines of documentation and code - ---- - -## Questions & Answers - -**Q: Is the fix safe to apply?** -A: Yes. The migration script is idempotent (safe to run multiple times) and only reads from the image server to populate the database. No data is modified or deleted. - -**Q: What if the fix doesn't work?** -A: Run `scripts/diagnose-production.sh` to identify the issue. Common problems and solutions are documented in PROD_TROUBLESHOOT.md. - -**Q: Do we need downtime?** -A: No. The service restart takes ~30 seconds, during which the site will be briefly unavailable. - -**Q: What if we need to rollback?** -A: The fix script creates a database backup automatically. Rollback procedure is in DEPLOYMENT_GUIDE.md. - -**Q: How do we prevent this in the future?** -A: Use the DEPLOYMENT_VERIFICATION_CHECKLIST.md for all future deployments. Smoke tests will catch this automatically once integrated into CI/CD. - ---- - -## Contact & Support - -**Primary Contact:** Scott McCarty (@fatherlinux) -**GitHub Issues:** https://github.com/crunchtools/rotv/issues -**PR #182:** https://github.com/crunchtools/rotv/pull/182 - -**For Urgent Issues:** -1. Check PROD_FIX_QUICKREF.md -2. Run scripts/diagnose-production.sh -3. Follow PRODUCTION_INCIDENT_README.md -4. Create GitHub issue if needed - ---- - -## Final Checklist - -Before marking this incident as closed: - -- [ ] Fix applied to production -- [ ] Images verified loading in browser -- [ ] Post-deployment report generated and reviewed -- [ ] No errors in service logs -- [ ] Smoke tests passing -- [ ] 24-hour monitoring period completed -- [ ] Incident documented -- [ ] Team notified of new resources -- [ ] Lessons learned captured -- [ ] Prevention measures planned - ---- - -**Last Updated:** 2026-04-04 -**Status:** ⏳ PENDING - Awaiting production fix application -**Next Review:** After fix is applied diff --git a/PACKAGE_STRUCTURE.md b/PACKAGE_STRUCTURE.md deleted file mode 100644 index 4170a883..00000000 --- a/PACKAGE_STRUCTURE.md +++ /dev/null @@ -1,405 +0,0 @@ -# Troubleshooting Package Structure - -**Visual Guide to All Resources** - ---- - -## 📊 Package Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ ENTRY POINTS (Start Here) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ README_PRODUCTION.md ............... Main production guide │ -│ PRODUCTION_INCIDENT_README.md ...... Active incident? Start │ -│ DEPLOYMENT_GUIDE.md ................ Deploying? Start │ -│ TROUBLESHOOTING_PACKAGE_INDEX.md ... Package overview │ -│ NEXT_STEPS.md ...................... Action plan │ -│ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ OPERATIONAL GUIDES │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ DEPLOYMENT TROUBLESHOOTING │ -│ ├─ DEPLOYMENT_GUIDE.md ├─ PROD_TROUBLESHOOT.md │ -│ ├─ DEPLOYMENT_VERIFICATION... ├─ PROD_FIX_QUICKREF.md │ -│ └─ NEXT_STEPS.md └─ PROD_ISSUE_FLOWCHART.md │ -│ │ -│ INCIDENT RESPONSE STAKEHOLDER COMMUNICATION │ -│ ├─ PRODUCTION_INCIDENT_... └─ EXEC_SUMMARY.md │ -│ └─ NEXT_STEPS.md │ -│ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ AUTOMATION TOOLS │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ SCRIPTS (bash) WORKFLOWS (GitHub Actions) │ -│ ├─ diagnose-production.sh └─ smoke-test.yml │ -│ ├─ fix-production.sh │ -│ ├─ verify-migrations.sh │ -│ └─ post-deployment-report.sh │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 🎯 User Journey Map - -### Journey 1: Incident Response (On-Call Engineer) - -``` -Incident Detected - ↓ -README_PRODUCTION.md (Quick links) - ↓ -PRODUCTION_INCIDENT_README.md (Choose path: Fast/Medium/Thorough) - ↓ - ├─ Path 1: Fast Fix (5 min) - │ └─ PROD_FIX_QUICKREF.md → Copy-paste commands → Done - │ - ├─ Path 2: Diagnose First (10 min) - │ └─ scripts/diagnose-production.sh - │ ↓ - │ scripts/fix-production.sh → Done - │ - └─ Path 3: Understand (30 min) - └─ EXEC_SUMMARY.md - ↓ - PROD_ISSUE_FLOWCHART.md - ↓ - PROD_TROUBLESHOOT.md - ↓ - Apply fix → Done - ↓ -Verify Fix - ↓ - ├─ Quick: curl health checks - └─ Thorough: scripts/post-deployment-report.sh - ↓ -Monitor (24 hours) - ↓ - └─ journalctl -f - ↓ -Document Incident - ↓ - └─ EXEC_SUMMARY.md template - ↓ -Close Incident -``` - -### Journey 2: Deployment (DevOps Engineer) - -``` -PR Merged → GitHub Actions Build - ↓ -DEPLOYMENT_GUIDE.md - ↓ - ├─ Standard Deployment (No migrations) - │ └─ Pull image → Restart → Verify - │ - └─ Migration Deployment (e.g., PR #182) - └─ Backup → Apply migrations → Verify migrations → Deploy - ↓ -DEPLOYMENT_VERIFICATION_CHECKLIST.md - ↓ - ├─ Manual verification - ├─ scripts/post-deployment-report.sh - └─ smoke-test.yml (GitHub Actions) - ↓ -Monitor (24 hours) - ↓ -Document deployment - ↓ -Done -``` - -### Journey 3: Troubleshooting (Support Engineer) - -``` -Issue Reported - ↓ -README_PRODUCTION.md → Troubleshooting section - ↓ -scripts/diagnose-production.sh - ↓ -Review Output - ↓ - ├─ Known Issue? - │ └─ PROD_FIX_QUICKREF.md → Apply fix - │ - └─ Unknown Issue? - └─ PROD_TROUBLESHOOT.md → Diagnostic steps - ↓ - Identify root cause - ↓ - Apply fix - ↓ -Verify Fix - ↓ -Update documentation (if new issue) - ↓ -Done -``` - -### Journey 4: Learning (New Team Member) - -``` -Onboarding - ↓ -README_PRODUCTION.md (Overview) - ↓ -DEPLOYMENT_GUIDE.md (How to deploy) - ↓ -Practice: Run scripts/diagnose-production.sh - ↓ -Review: DEPLOYMENT_VERIFICATION_CHECKLIST.md - ↓ -Study: PROD_TROUBLESHOOT.md - ↓ -Understand: PROD_ISSUE_FLOWCHART.md - ↓ -Ready for on-call -``` - ---- - -## 📋 Document Cross-Reference Matrix - -| Document | Deployment | Troubleshoot | Incident | Learning | -|----------|------------|--------------|----------|----------| -| **README_PRODUCTION.md** | ✅ Links | ✅ Quick ref | ✅ Entry | ✅ Overview | -| **DEPLOYMENT_GUIDE.md** | ✅ Primary | ⚪ Rollback | ⚪ Context | ✅ Procedures | -| **DEPLOYMENT_VERIFICATION_CHECKLIST.md** | ✅ Post-deploy | ⚪ Prevention | ⚪ Verify | ✅ Checklist | -| **PRODUCTION_INCIDENT_README.md** | ⚪ Context | ✅ Fast path | ✅ Primary | ⚪ Examples | -| **EXEC_SUMMARY.md** | ⚪ Template | ⚪ Template | ✅ Comms | ⚪ Template | -| **PROD_TROUBLESHOOT.md** | ⚪ Issues | ✅ Primary | ✅ Diagnose | ✅ Reference | -| **PROD_FIX_QUICKREF.md** | ⚪ Quick ref | ✅ Commands | ✅ Fast fix | ⚪ Cheat sheet | -| **PROD_ISSUE_FLOWCHART.md** | ⚪ Flow | ✅ Visual | ✅ Understand | ✅ Visual aid | -| **NEXT_STEPS.md** | ⚪ Actions | ⚪ Actions | ✅ Closure | ⚪ Roadmap | - -Legend: ✅ Primary use • ⚪ Secondary use - ---- - -## 🛠️ Script Dependency Graph - -``` -scripts/diagnose-production.sh - ├─ Checks: Container status - ├─ Checks: Database tables - ├─ Checks: Migrations applied - ├─ Checks: API endpoints - ├─ Checks: Error rates - └─ Output: Pass/fail with recommendations - ↓ - (If issues found) - ↓ -scripts/fix-production.sh - ├─ Creates: Database backup - ├─ Applies: Migration 016 - ├─ Runs: migrate-primary-images.js - ├─ Restarts: Service - └─ Calls: verify-migrations.sh - ↓ -scripts/verify-migrations.sh - ├─ Checks: Table schemas - ├─ Checks: Indexes - ├─ Checks: Constraints - ├─ Checks: Data population - └─ Output: Migration status - ↓ - (After deployment) - ↓ -scripts/post-deployment-report.sh - ├─ Gathers: Service status - ├─ Gathers: Database health - ├─ Gathers: API status - ├─ Gathers: Recent errors - ├─ Analyzes: Migration status - └─ Generates: Markdown report -``` - ---- - -## 🔄 Workflow Integration - -``` -GitHub Actions - ↓ -.github/workflows/build.yml - ├─ On: Push to master - ├─ Builds: Container image - ├─ Pushes: To quay.io - └─ Triggers: Test workflow - ↓ -.github/workflows/test.yml - ├─ Runs: Unit tests - ├─ Runs: Integration tests - └─ Reports: Test results - ↓ - (Manual trigger after deployment) - ↓ -.github/workflows/smoke-test.yml - ├─ Tests: Health endpoint - ├─ Tests: API endpoints - ├─ Tests: Media endpoint (PR #182) - ├─ Tests: SSRF protection - └─ Reports: Production health -``` - ---- - -## 📂 File Organization - -``` -rotv/ -├── Root Level (Entry points) -│ ├── README.md ............................ Main project README -│ ├── README_PRODUCTION.md ................. Production ops (START HERE) -│ ├── NEXT_STEPS.md ........................ Action plan for incident -│ └── TROUBLESHOOTING_PACKAGE_INDEX.md ..... Package overview -│ -├── Deployment Documentation -│ ├── DEPLOYMENT_GUIDE.md .................. Complete deployment guide -│ └── DEPLOYMENT_VERIFICATION_CHECKLIST.md . Post-deployment checklist -│ -├── Incident Response -│ ├── PRODUCTION_INCIDENT_README.md ........ Incident response guide -│ └── EXEC_SUMMARY.md ...................... Executive summary template -│ -├── Troubleshooting -│ ├── PROD_TROUBLESHOOT.md ................. Comprehensive troubleshooting -│ ├── PROD_FIX_QUICKREF.md ................. Quick reference commands -│ └── PROD_ISSUE_FLOWCHART.md .............. Visual debugging guide -│ -├── scripts/ -│ ├── diagnose-production.sh ............... Automated diagnostics -│ ├── fix-production.sh .................... Automated fix -│ ├── verify-migrations.sh ................. Migration verification -│ └── post-deployment-report.sh ............ Deployment reporting -│ -└── .github/workflows/ - └── smoke-test.yml ....................... Post-deployment smoke tests -``` - ---- - -## 🎨 Color-Coded Priority Levels - -### 🔴 Critical - Read First -- **README_PRODUCTION.md** - Production operations overview -- **PRODUCTION_INCIDENT_README.md** - Active incident response -- **NEXT_STEPS.md** - Immediate action items - -### 🟡 Important - Read for Context -- **DEPLOYMENT_GUIDE.md** - Deployment procedures -- **PROD_TROUBLESHOOT.md** - Troubleshooting guide -- **DEPLOYMENT_VERIFICATION_CHECKLIST.md** - Verification checklist - -### 🟢 Reference - Use as Needed -- **PROD_FIX_QUICKREF.md** - Quick commands -- **PROD_ISSUE_FLOWCHART.md** - Visual diagrams -- **EXEC_SUMMARY.md** - Communication template -- **TROUBLESHOOTING_PACKAGE_INDEX.md** - Package index - -### ⚪ Automation - Run When Needed -- **scripts/*.sh** - Diagnostic and fix scripts -- **smoke-test.yml** - GitHub Actions workflow - ---- - -## 📈 Recommended Reading Order by Role - -### On-Call Engineer (First Incident) -1. README_PRODUCTION.md (5 min) -2. PRODUCTION_INCIDENT_README.md (10 min) -3. PROD_FIX_QUICKREF.md (5 min) -4. Try: scripts/diagnose-production.sh - -### DevOps Engineer (Deploying) -1. DEPLOYMENT_GUIDE.md (15 min) -2. DEPLOYMENT_VERIFICATION_CHECKLIST.md (10 min) -3. Try: scripts/post-deployment-report.sh -4. Setup: smoke-test.yml automation - -### Support Engineer (Troubleshooting) -1. README_PRODUCTION.md (5 min) -2. PROD_TROUBLESHOOT.md (20 min) -3. PROD_ISSUE_FLOWCHART.md (10 min) -4. Try: scripts/diagnose-production.sh - -### Technical Lead (Planning) -1. EXEC_SUMMARY.md (5 min) -2. NEXT_STEPS.md (15 min) -3. TROUBLESHOOTING_PACKAGE_INDEX.md (10 min) -4. Review all automation scripts - -### New Team Member (Onboarding) -1. README_PRODUCTION.md (5 min) -2. DEPLOYMENT_GUIDE.md (15 min) -3. PROD_TROUBLESHOOT.md (20 min) -4. Practice with all scripts (30 min) -5. Review DEPLOYMENT_VERIFICATION_CHECKLIST.md (10 min) - ---- - -## 🔗 Quick Navigation Links - -### Most Used Documents -- **[README_PRODUCTION.md](./README_PRODUCTION.md)** - Start here -- **[PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md)** - Quick fixes -- **[DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md)** - How to deploy - -### Emergency Reference -- **[PRODUCTION_INCIDENT_README.md](./PRODUCTION_INCIDENT_README.md)** - Incident response -- **[NEXT_STEPS.md](./NEXT_STEPS.md)** - Current action items -- **[EXEC_SUMMARY.md](./EXEC_SUMMARY.md)** - Stakeholder comms - -### Deep Dive -- **[PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md)** - Comprehensive guide -- **[PROD_ISSUE_FLOWCHART.md](./PROD_ISSUE_FLOWCHART.md)** - Visual debugging -- **[TROUBLESHOOTING_PACKAGE_INDEX.md](./TROUBLESHOOTING_PACKAGE_INDEX.md)** - Full index - ---- - -## 📊 Package Metrics - -| Metric | Value | -|--------|-------| -| Total files | 16 | -| Documentation files | 11 | -| Script files | 4 | -| Workflow files | 1 | -| Documentation lines | ~4,500 | -| Script lines | ~1,200 | -| Coverage | Complete incident lifecycle | -| Time to fix (estimated) | 5 minutes | -| Time to diagnose (automated) | 30 seconds | - ---- - -## ✅ Quality Assurance - -This package provides: -- ✅ Multiple entry points for different roles -- ✅ Clear navigation paths -- ✅ Automated diagnostics and fixes -- ✅ Visual aids (flowcharts, diagrams) -- ✅ Copy-paste commands (no guessing) -- ✅ Verification checklists -- ✅ Rollback procedures -- ✅ Stakeholder communication templates -- ✅ Prevention measures -- ✅ Cross-references between documents - ---- - -**Created:** 2026-04-04 -**Version:** 1.0 -**Maintained by:** Scott McCarty (@fatherlinux) diff --git a/PRODUCTION_INCIDENT_README.md b/PRODUCTION_INCIDENT_README.md deleted file mode 100644 index 722092e3..00000000 --- a/PRODUCTION_INCIDENT_README.md +++ /dev/null @@ -1,331 +0,0 @@ -# Production Incident: Image Loading Failure (PR #182) - -**Status:** 🔴 ACTIVE INCIDENT -**Severity:** Major (user-facing feature broken) -**Impact:** All POI images failing to load -**Root Cause:** Database migration script not executed during deployment -**Time to Fix:** 5 minutes -**Last Updated:** 2026-04-04 - ---- - -## 🚨 Quick Start (Choose Your Path) - -### Path 1: Just Fix It (Fastest) -**Time: 5 minutes** - -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js -systemctl restart rootsofthevalley.org -``` - -See: **[PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md)** for detailed commands. - ---- - -### Path 2: Diagnose First, Then Fix -**Time: 10 minutes** - -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com -bash scripts/diagnose-production.sh # Automated diagnostics -bash scripts/fix-production.sh # Automated fix with backup -``` - -See: **[PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md)** for comprehensive troubleshooting. - ---- - -### Path 3: Understand First (Technical Deep Dive) -**Time: 20 minutes + fix** - -1. Read **[EXEC_SUMMARY.md](./EXEC_SUMMARY.md)** - What happened and why -2. Review **[PROD_ISSUE_FLOWCHART.md](./PROD_ISSUE_FLOWCHART.md)** - Visual diagrams -3. Check **[DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md)** - Prevent recurrence -4. Then apply fix from Path 1 or Path 2 - ---- - -## 📋 Document Index - -| Document | Purpose | When to Use | -|----------|---------|-------------| -| **[EXEC_SUMMARY.md](./EXEC_SUMMARY.md)** | Executive overview | Stakeholder briefing, decision making | -| **[PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md)** | Copy-paste commands | Quick resolution, on-call reference | -| **[PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md)** | Detailed troubleshooting | Deep dive, unusual symptoms | -| **[PROD_ISSUE_FLOWCHART.md](./PROD_ISSUE_FLOWCHART.md)** | Visual diagrams | Understanding data flow, root cause | -| **[DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md)** | Post-deployment checks | Prevent future incidents | -| **[scripts/diagnose-production.sh](./scripts/diagnose-production.sh)** | Automated diagnostics | Quick health check | -| **[scripts/fix-production.sh](./scripts/fix-production.sh)** | Automated fix | Safe, guided fix procedure | - ---- - -## 🎯 What You Need to Know (30 Second Version) - -**Problem:** Images not loading on rootsofthevalley.org - -**Why:** Database table `poi_media` is empty (migration script skipped) - -**Fix:** Run one script to populate the table - -**Risk:** None (read-only operation, backup created) - -**Time:** 5 minutes total - ---- - -## 📊 Incident Timeline - -| Time | Event | -|------|-------| -| Earlier today | PR #182 merged and deployed | -| Earlier today | Container restarted successfully | -| Earlier today | User reports images not loading | -| Now | Incident identified: migration script not run | -| Now + 5min | Fix applied and verified | -| Now + 24hr | Monitoring for related issues | - ---- - -## 🔍 Symptoms - -### User-Facing -- POI images show "Failed to load image" error -- Image thumbnails return 404 -- Mosaic component shows nothing -- Map and text content work fine - -### Technical -```bash -# API returns empty media arrays -curl https://rootsofthevalley.org/api/pois/1/media -{"mosaic":[],"all_media":[],"total_count":0} - -# Database table is empty -SELECT COUNT(*) FROM poi_media WHERE role='primary'; -# Returns: 0 (should be ~75) - -# Logs show 404 errors -journalctl -u rootsofthevalley.org | grep "Image not found" -``` - ---- - -## 🛠️ Fix Procedure (Step-by-Step) - -### Pre-Fix Checklist -- [ ] SSH access to lotor.dc3.crunchtools.com -- [ ] Root/sudo privileges -- [ ] Container `rootsofthevalley.org` is running -- [ ] 5 minutes available for fix + verification - -### Fix Steps - -1. **SSH to Production** - ```bash - ssh -p 22422 root@lotor.dc3.crunchtools.com - ``` - -2. **Verify Problem** - ```bash - podman exec rootsofthevalley.org psql -U postgres -d rotv -tAc \ - "SELECT COUNT(*) FROM poi_media WHERE role='primary';" - # Should return 0 (confirms diagnosis) - ``` - -3. **Create Backup** - ```bash - mkdir -p /root/backups - podman exec rootsofthevalley.org pg_dump -U postgres rotv > \ - /root/backups/rotv_$(date +%Y%m%d_%H%M%S).sql - ``` - -4. **Run Migration Script** - ```bash - podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js - # Watch output - should show "Migrated: N" where N > 0 - ``` - -5. **Apply Data Integrity Migration** - ```bash - podman exec rootsofthevalley.org psql -U postgres -d rotv \ - -f /app/migrations/016_fix_poi_media_constraints.sql - ``` - -6. **Restart Service** - ```bash - systemctl restart rootsofthevalley.org - sleep 10 - systemctl status rootsofthevalley.org - ``` - -7. **Verify Fix** - ```bash - # Database check - podman exec rootsofthevalley.org psql -U postgres -d rotv -tAc \ - "SELECT COUNT(*) FROM poi_media WHERE role='primary';" - # Should return number > 0 - - # API check - curl -s https://rootsofthevalley.org/api/pois/1/media | jq '.total_count' - # Should return number > 0 - - # Browser check: Visit https://rootsofthevalley.org and click a POI - # Images should load - ``` - -### Post-Fix Checklist -- [ ] Database table populated (count > 0) -- [ ] API returns media items -- [ ] Images load in browser -- [ ] No errors in service logs -- [ ] Service stable for 15+ minutes - ---- - -## 📞 Escalation - -### If Fix Fails -1. Check **[PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md)** for common errors -2. Review migration script output for specific error messages -3. Check image server connectivity: `curl http://10.89.1.100:8000/api/health` -4. Consider rollback (see below) - -### Rollback Procedure -```bash -# Restore database backup -podman exec -i rootsofthevalley.org psql -U postgres rotv < \ - /root/backups/rotv_TIMESTAMP.sql - -# Restart service -systemctl restart rootsofthevalley.org - -# Verify rollback -curl https://rootsofthevalley.org/api/health -``` - -### Contact -- **GitHub Issue:** https://github.com/crunchtools/rotv/issues -- **PR #182:** https://github.com/crunchtools/rotv/pull/182 -- **Deployment Owner:** Scott McCarty (@fatherlinux) - ---- - -## 📈 Prevention (Next Steps) - -### Immediate (This Incident) -- [x] Document root cause ✅ (this file) -- [x] Create fix scripts ✅ (diagnose/fix shell scripts) -- [ ] Apply fix to production -- [ ] Verify fix works -- [ ] Monitor for 24 hours - -### Short Term (Next Week) -- [ ] Add smoke tests to CI/CD -- [ ] Update deployment runbook with verification steps -- [ ] Create post-deployment checklist automation -- [ ] Review all migration scripts for similar issues - -### Long Term (Next Quarter) -- [ ] Automate database migrations in deployment -- [ ] Add monitoring/alerting for table count anomalies -- [ ] Create canary deployment process -- [ ] Build automated rollback capabilities - ---- - -## 🧪 Testing (Before Marking Incident Closed) - -### Automated Tests -```bash -# Run diagnostic script -bash scripts/diagnose-production.sh - -# All checks should pass -``` - -### Manual Tests -1. **Image Loading** - - Navigate to https://rootsofthevalley.org - - Click 5 different POI markers - - Verify images load for each - - No errors in browser console - -2. **API Endpoints** - - Test `/api/pois/1/media` returns data - - Test `/api/pois/1/thumbnail` returns image - - Test `/api/assets/:id/thumbnail` works - -3. **Admin Functions** - - Login as admin - - Upload test image - - Verify appears in moderation queue - - Approve image - - Verify appears in mosaic - -### Performance Tests -```bash -# Response time should be < 1 second -time curl -s https://rootsofthevalley.org/api/pois/1/media > /dev/null - -# No memory leaks (check over time) -podman stats --no-stream rootsofthevalley.org -``` - ---- - -## 📝 Incident Report Template - -**Incident ID:** ROTV-2026-04-04-IMAGE-LOADING - -**Severity:** Major - -**Start Time:** 2026-04-04 [TIME] - -**Detection:** User report / Manual discovery - -**Root Cause:** Database migration script (`migrate-primary-images.js`) not executed during PR #182 deployment - -**Impact:** -- All POI images failing to load -- ~100% of image requests returning 404 -- User experience degraded (no images visible) - -**Resolution:** -1. Identified empty `poi_media` table -2. Ran migration script to populate table -3. Applied data integrity migration -4. Restarted service -5. Verified images loading correctly - -**Time to Resolution:** [FILL IN] - -**Downtime:** None (service remained available, only image feature affected) - -**Lessons Learned:** -1. Deployment runbook steps were skipped (steps 5-6) -2. No post-deployment verification performed -3. Need automated smoke tests in CI/CD -4. Need deployment verification checklist - -**Action Items:** -- [ ] Add smoke tests to GitHub Actions -- [ ] Automate deployment verification -- [ ] Update runbook with mandatory verification steps -- [ ] Create alerts for table count anomalies - ---- - -## 🎓 References - -- **Original PR:** https://github.com/crunchtools/rotv/pull/182 -- **Deployment Runbook:** `.specify/specs/004-multi-image-poi/DEPLOYMENT_RUNBOOK.md` -- **Feature Spec:** `.specify/specs/004-multi-image-poi/spec.md` -- **Implementation Plan:** `.specify/specs/004-multi-image-poi/plan.md` - ---- - -**Last Updated:** 2026-04-04 -**Status:** 🔴 Active (awaiting fix application) -**Next Review:** After fix verified diff --git a/PROD_FIX_QUICKREF.md b/PROD_FIX_QUICKREF.md deleted file mode 100644 index 4cca682f..00000000 --- a/PROD_FIX_QUICKREF.md +++ /dev/null @@ -1,255 +0,0 @@ -# Production Fix Quick Reference (PR #182) - -## TL;DR - The One-Liner Fix - -```bash -# SSH to production -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# Run the fix script (interactive, creates backup) -bash < <(curl -s https://raw.githubusercontent.com/crunchtools/rotv/master/scripts/fix-production.sh) - -# OR manually run migration -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js -``` - ---- - -## Copy-Paste Commands (Manual Fix) - -### 1. SSH to Production -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com -``` - -### 2. Run Diagnostics -```bash -# Check current state -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "SELECT COUNT(*) FROM poi_media WHERE role='primary';" - -# Expected: 0 (problem) → Need to run migration -# Expected: 50+ (good) → Migration already done -``` - -### 3. Backup Database -```bash -mkdir -p /root/backups -podman exec rootsofthevalley.org pg_dump -U postgres rotv > /root/backups/rotv_$(date +%Y%m%d_%H%M%S).sql -``` - -### 4. Apply Migrations -```bash -# Migration 016 (constraints) -podman exec rootsofthevalley.org psql -U postgres -d rotv -f /app/migrations/016_fix_poi_media_constraints.sql - -# Migration script (primary images) - DRY RUN FIRST -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js --dry-run - -# If dry run looks good, run for real -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js -``` - -### 5. Restart Service -```bash -systemctl restart rootsofthevalley.org -sleep 10 -systemctl status rootsofthevalley.org -``` - -### 6. Verify Fix -```bash -# Check database -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "SELECT COUNT(*) FROM poi_media WHERE role='primary';" - -# Test API -curl -s https://rootsofthevalley.org/api/pois/1/media | jq '.total_count' - -# Should return a number > 0 -``` - ---- - -## Diagnosis in 30 Seconds - -```bash -# One command to check everything -ssh -p 22422 root@lotor.dc3.crunchtools.com "podman exec rootsofthevalley.org psql -U postgres -d rotv -c \"SELECT 'Total media:' as check, COUNT(*)::text as count FROM poi_media UNION ALL SELECT 'Primary images:', COUNT(*)::text FROM poi_media WHERE role='primary' UNION ALL SELECT 'Expected primary:', COUNT(*)::text FROM pois WHERE has_primary_image = true;\"" -``` - -**Expected output if migration needed:** -``` - check | count ------------------+------- - Total media: | 0 - Primary images: | 0 - Expected primary: | 75 -``` - -**Expected output if already fixed:** -``` - check | count ------------------+------- - Total media: | 75 - Primary images: | 75 - Expected primary: | 75 -``` - ---- - -## Troubleshooting Common Errors - -### Error: "ECONNREFUSED" during migration -**Problem:** Image server is unreachable - -**Fix:** -```bash -# Test connectivity -podman exec rootsofthevalley.org curl -s http://10.89.1.100:8000/api/health - -# Check environment variable -podman exec rootsofthevalley.org printenv IMAGE_SERVER_URL - -# Should show: http://10.89.1.100:8000 -``` - -### Error: "relation poi_media does not exist" -**Problem:** Migration 015 not applied - -**Fix:** -```bash -podman exec rootsofthevalley.org psql -U postgres -d rotv -f /app/migrations/015_add_poi_media.sql -``` - -### Error: Unique constraint violation -**Problem:** Trying to create duplicate primary images - -**Fix:** -```bash -# Check for duplicates -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT poi_id, COUNT(*) -FROM poi_media -WHERE role = 'primary' AND moderation_status IN ('published', 'auto_approved') -GROUP BY poi_id -HAVING COUNT(*) > 1;" - -# Script should skip existing entries, but if not: -# Re-run with --dry-run to see what it will do -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js --dry-run -``` - ---- - -## Monitoring After Fix - -### Watch logs in real-time -```bash -journalctl -u rootsofthevalley.org -f -``` - -### Check for errors in last hour -```bash -journalctl -u rootsofthevalley.org --since "1 hour ago" | grep -i error | tail -20 -``` - -### Test specific POI -```bash -# Replace 42 with actual POI ID -curl -s https://rootsofthevalley.org/api/pois/42/media | jq -``` - -### Check database stats -```bash -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT - media_type, - role, - moderation_status, - COUNT(*) -FROM poi_media -GROUP BY media_type, role, moderation_status -ORDER BY media_type, role, moderation_status; -" -``` - ---- - -## Rollback (If Needed) - -```bash -# Find backup -ls -lht /root/backups/rotv_* | head -5 - -# Restore -podman exec -i rootsofthevalley.org psql -U postgres rotv < /root/backups/rotv_TIMESTAMP.sql - -# Restart -systemctl restart rootsofthevalley.org -``` - ---- - -## Expected Results After Fix - -### Database -- `poi_media` table has ~75 records (or however many POIs have images) -- All primary images have `role='primary'` and `moderation_status='published'` -- Migration 016 constraints are in place - -### API -```bash -curl https://rootsofthevalley.org/api/pois/1/media -``` -Returns: -```json -{ - "mosaic": [...], - "all_media": [...], - "total_count": 1 -} -``` - -### Frontend -- Click any POI → images display correctly -- No "Failed to load image" errors in browser console -- Mosaic displays for POIs with multiple images -- Single image displays for POIs with one image -- Default thumbnail for POIs with no images - ---- - -## File Locations - -| File | Location | -|------|----------| -| Diagnostic script | `scripts/diagnose-production.sh` | -| Fix script | `scripts/fix-production.sh` | -| Migration 015 | `backend/migrations/015_add_poi_media.sql` | -| Migration 016 | `backend/migrations/016_fix_poi_media_constraints.sql` | -| Migration script | `backend/scripts/migrate-primary-images.js` | -| Full troubleshooting | `PROD_TROUBLESHOOT.md` | -| Deployment runbook | `.specify/specs/004-multi-image-poi/DEPLOYMENT_RUNBOOK.md` | - ---- - -## Quick Tests (Frontend) - -1. Open https://rootsofthevalley.org -2. Open browser DevTools (F12) → Console tab -3. Click any POI marker -4. Check for errors in console -5. Verify images display in sidebar - -**Common console errors:** -- `Failed to load media:` → API endpoint issue -- `Image not found` → Thumbnail endpoint issue -- `net::ERR_FAILED` → Asset proxy issue - ---- - -## Support - -**GitHub Issue:** https://github.com/crunchtools/rotv/issues -**PR #182:** https://github.com/crunchtools/rotv/pull/182 -**Server:** lotor.dc3.crunchtools.com:22422 -**Service:** rootsofthevalley.org diff --git a/PROD_ISSUE_FLOWCHART.md b/PROD_ISSUE_FLOWCHART.md deleted file mode 100644 index 2c82832d..00000000 --- a/PROD_ISSUE_FLOWCHART.md +++ /dev/null @@ -1,305 +0,0 @@ -# Production Issue Flowchart (PR #182) - -## Issue Flow: "Failed to load image" - -``` -User clicks POI on map - ↓ -Frontend: Sidebar.jsx loads - ↓ -API Call #1: GET /api/pois/${id}/media - ↓ -Backend: server.js:1008-1086 - ↓ - Query: SELECT * FROM poi_media WHERE poi_id = $1 - ↓ - Result: [] (empty - table has no rows) - ↓ - Response: { mosaic: [], all_media: [], total_count: 0 } - ↓ -Frontend receives empty media array - ↓ -Checks: media.length > 0? → NO - ↓ -Checks: has_primary_image = true? → YES (database flag still set) - ↓ -Fallback: Load single image via legacy endpoint - ↓ -API Call #2: GET /api/pois/${id}/thumbnail - ↓ -Backend: server.js:957-997 (CHANGED IN PR #182) - ↓ - OLD CODE (before PR #182): - ↓ - Query image server directly - ↓ - Return image data - - NEW CODE (PR #182): - ↓ - Query: SELECT image_server_asset_id - FROM poi_media - WHERE poi_id = $1 AND role = 'primary' - ↓ - Result: [] (empty - no rows!) - ↓ - Return 404: "Image not found" - ↓ - ❌ ERROR: "Failed to load image" -``` - ---- - -## Root Cause Diagram - -``` -PR #182 Changed Image Serving Logic - ↓ -┌───────────────────────────────────────────────────┐ -│ BEFORE: Direct image server queries │ -│ │ -│ /api/pois/:id/thumbnail │ -│ → imageServerClient.getPrimaryAsset(poiId) │ -│ → Fetch from image server │ -│ → Return image │ -└───────────────────────────────────────────────────┘ - ↓ -┌───────────────────────────────────────────────────┐ -│ AFTER: Database-backed with poi_media table │ -│ │ -│ /api/pois/:id/thumbnail │ -│ → SELECT FROM poi_media WHERE role='primary' │ -│ → Get image_server_asset_id │ -│ → Fetch from image server using assetId │ -│ → Return image │ -└───────────────────────────────────────────────────┘ - ↓ -┌───────────────────────────────────────────────────┐ -│ REQUIRED: poi_media table must be populated │ -│ │ -│ Migration 015: ✅ Creates table structure │ -│ Migration script: ❌ Populates with data │ -│ (NOT RUN IN PROD) │ -└───────────────────────────────────────────────────┘ - ↓ - RESULT: 404 errors on all image requests -``` - ---- - -## What Should Have Happened (Deployment Steps) - -``` -1. ✅ Merge PR #182 -2. ✅ GHA builds new container -3. ✅ Backup database -4. ✅ Apply migration 015 (creates poi_media table) -5. ❌ Run migrate-primary-images.js ← SKIPPED -6. ❌ Apply migration 016 (data integrity) ← MAYBE SKIPPED -7. ✅ Pull new container -8. ✅ Restart service -9. ❌ Verify images load ← WOULD HAVE CAUGHT THIS -``` - -**Missing:** Steps 5, 6, and 9 from deployment runbook - ---- - -## Data Flow (When Working Correctly) - -``` -Database: pois table -┌─────────────────────────────────────────┐ -│ id │ name │ has_primary_image │ -├────┼───────────────┼───────────────────┤ -│ 1 │ Trailhead A │ true │ -│ 2 │ Historical B │ true │ -└─────────────────────────────────────────┘ - ↓ -Migration Script: migrate-primary-images.js - ↓ - 1. Queries image server for primary assets - 2. For each POI with has_primary_image=true - 3. Creates record in poi_media table - ↓ -Database: poi_media table -┌────────────────────────────────────────────────────────────┐ -│ poi_id │ role │ image_server_asset_id │ moderation │ -├────────┼─────────┼───────────────────────┼──────────────┤ -│ 1 │ primary │ abc123 │ published │ -│ 2 │ primary │ def456 │ published │ -└────────────────────────────────────────────────────────────┘ - ↓ -API: GET /api/pois/1/thumbnail - ↓ - SELECT image_server_asset_id FROM poi_media - WHERE poi_id = 1 AND role = 'primary' - ↓ - Result: 'abc123' - ↓ - Fetch from image server: /api/assets/abc123/thumbnail - ↓ - Return image data to browser - ↓ - ✅ Image displays -``` - ---- - -## Current Broken State - -``` -Database: pois table -┌─────────────────────────────────────────┐ -│ id │ name │ has_primary_image │ -├────┼───────────────┼───────────────────┤ -│ 1 │ Trailhead A │ true │ ← Flag still set -│ 2 │ Historical B │ true │ -└─────────────────────────────────────────┘ - ↓ -❌ Migration script NOT RUN - ↓ -Database: poi_media table -┌────────────────────────────────────────────────────────────┐ -│ poi_id │ role │ image_server_asset_id │ moderation │ -├────────┼─────────┼───────────────────────┼──────────────┤ -│ (empty)│ │ │ │ ← NO DATA -└────────────────────────────────────────────────────────────┘ - ↓ -API: GET /api/pois/1/thumbnail - ↓ - SELECT image_server_asset_id FROM poi_media - WHERE poi_id = 1 AND role = 'primary' - ↓ - Result: [] (no rows) - ↓ - Return 404: "Image not found" - ↓ - ❌ "Failed to load image" error in browser -``` - ---- - -## Fix Flow - -``` -Run: migrate-primary-images.js - ↓ - 1. Queries: SELECT id FROM pois WHERE has_primary_image = true - ↓ - Found: [1, 2, 3, ..., 75] - ↓ - 2. For each POI: - ↓ - a. Check if already has primary in poi_media - → Skip if exists (prevents duplicates) - ↓ - b. Fetch primary asset from image server - → GET http://10.89.1.100:8000/api/assets?poi_id=1&role=primary - ↓ - c. Create poi_media record - → INSERT INTO poi_media (poi_id, role, image_server_asset_id, ...) - VALUES (1, 'primary', 'abc123', ...) - ↓ - 3. Summary: - ✓ Migrated: 75 - ✓ Skipped: 0 (already exists) - ✓ Failed: 0 (no asset found) - ↓ -Database: poi_media now populated - ↓ -Restart service - ↓ -Images load correctly ✅ -``` - ---- - -## Verification Flow - -``` -1. Database Check - ↓ - podman exec rootsofthevalley.org psql -U postgres -d rotv \ - -c "SELECT COUNT(*) FROM poi_media WHERE role='primary';" - ↓ - Expected: 75 (or number of POIs with images) - ↓ -2. API Check - ↓ - curl https://rootsofthevalley.org/api/pois/1/media - ↓ - Expected: { "mosaic": [...], "all_media": [...], "total_count": N } - ↓ -3. Frontend Check - ↓ - Open browser → Click POI - ↓ - Expected: Images display, no console errors - ↓ - ✅ Fix verified -``` - ---- - -## Key Files Changed in PR #182 - -### Backend Changes -- `backend/server.js:957-997` - Thumbnail endpoint now queries `poi_media` -- `backend/server.js:1008-1086` - New media endpoint -- `backend/server.js:1229-1299` - Asset proxy endpoints -- `backend/migrations/015_add_poi_media.sql` - Creates table -- `backend/migrations/016_fix_poi_media_constraints.sql` - Data integrity -- `backend/scripts/migrate-primary-images.js` - Populates table - -### Frontend Changes -- `frontend/src/components/Sidebar.jsx:232-241` - Calls new media endpoint -- `frontend/src/components/Mosaic.jsx` - New component -- `frontend/src/components/Lightbox.jsx` - New component -- `frontend/src/components/MediaUploadModal.jsx` - New component - ---- - -## Migration Dependencies - -``` -Migration 015 (SQL) - ↓ -Creates poi_media table structure - ↓ -Migration Script (Node.js) - ↓ -Populates poi_media with data from image server - ↓ -Migration 016 (SQL) - ↓ -Adds data integrity constraints - ↓ -Service Restart - ↓ -New code can serve images from poi_media -``` - -**Critical:** Steps must be done in order. Migration script depends on table existing (015) but should be run before constraints (016) to avoid validation errors during bulk insert. - ---- - -## Why This Wasn't Caught Earlier - -1. **Local testing:** Uses ephemeral database in container - - Migration script runs as part of build - - Always starts fresh - - Would work correctly in dev - -2. **CI/CD:** Doesn't test against production database - - Can't verify data migration - - Only tests code, not deployment - -3. **Deployment:** Manual steps - - Runbook exists but steps were skipped - - No automated verification - -4. **Solution:** Add post-deployment smoke tests - - Check table counts - - Test key API endpoints - - Verify sample POI loads diff --git a/PROD_TROUBLESHOOT.md b/PROD_TROUBLESHOOT.md deleted file mode 100644 index c0e6fc8c..00000000 --- a/PROD_TROUBLESHOOT.md +++ /dev/null @@ -1,345 +0,0 @@ -# Production Troubleshooting: Failed to Load Image (PR #182) - -**Issue:** "Failed to load image" error in production after PR #182 deployment -**Root Cause:** Database migration script not executed -**Date:** 2026-04-04 - ---- - -## Quick Diagnosis - -### Step 1: Check if `poi_media` table exists and has data - -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com - -# Check table exists -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "\dt poi_media" - -# Check if table has any records -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "SELECT COUNT(*) FROM poi_media;" - -# Check specifically for primary images -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "SELECT COUNT(*) FROM poi_media WHERE role='primary';" -``` - -**Expected Results:** -- Table exists: ✅ (migration 015 applied) -- Total count: Should be > 0 if migration script ran -- Primary count: Should match number of POIs with images (likely 50-100+) - -**If count is 0:** The migration script was NOT run ⚠️ - ---- - -## Step 2: Verify Migration 015 Applied - -```bash -# Check if poi_media table has correct schema -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "\d poi_media" -``` - -**Expected Columns:** -- id, poi_id, media_type -- image_server_asset_id, youtube_url -- role, sort_order, likes_count, caption -- moderation_status, confidence_score, ai_reasoning -- submitted_by, moderated_by, moderated_at, created_at - -**If table doesn't exist:** Migration 015 was not applied - ---- - -## Step 3: Verify Migration 016 Applied - -```bash -# Check for caption length constraint -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT conname -FROM pg_constraint -WHERE conrelid = 'poi_media'::regclass - AND conname = 'poi_media_caption_length_check'; -" -``` - -**Expected:** 1 row returned with constraint name - -**If not found:** Migration 016 was not applied - ---- - -## Step 4: Check Image Server Connectivity - -```bash -# Check IMAGE_SERVER_URL environment variable -podman exec rootsofthevalley.org printenv | grep IMAGE_SERVER - -# Test image server from container -podman exec rootsofthevalley.org curl -s http://10.89.1.100:8000/api/health | jq - -# Alternative: Check from host -curl -s http://10.89.1.100:8000/api/health | jq -``` - -**Expected Results:** -- `IMAGE_SERVER_URL=http://10.89.1.100:8000` (or similar) -- Health endpoint returns `{"status": "ok"}` or similar - -**If connection fails:** Image server is down or unreachable - ---- - -## Step 5: Check Application Logs - -```bash -# Check for initialization errors -journalctl -u rootsofthevalley.org --since "1 hour ago" | grep -i "imageserver" - -# Check for 404 errors on media endpoints -journalctl -u rootsofthevalley.org --since "1 hour ago" | grep "/api/pois/.*/media" - -# Check for thumbnail endpoint errors -journalctl -u rootsofthevalley.org --since "1 hour ago" | grep "/api/pois/.*/thumbnail" -``` - -**Look for:** -- `[ImageServer] Initialized with server: http://10.89.1.100:8000` ✅ -- `[ImageServer] Not configured - set IMAGE_SERVER_URL` ❌ -- 404 errors on thumbnail requests -- Database query errors - ---- - -## Fix Procedure - -### Fix 1: Apply Missing Migrations (if `poi_media` is empty) - -```bash -# DRY RUN first to see what would be migrated -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js --dry-run - -# Review output, then run for real -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js - -# Verify migration succeeded -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT - media_type, - role, - moderation_status, - COUNT(*) -FROM poi_media -GROUP BY media_type, role, moderation_status; -" -``` - -**Expected Output:** -``` - media_type | role | moderation_status | count -------------+---------+-------------------+------- - image | primary | published | 75 -(1 row) -``` - -### Fix 2: Apply Migration 016 (if constraints missing) - -```bash -# Apply data integrity migration -podman exec rootsofthevalley.org psql -U postgres -d rotv -f /app/migrations/016_fix_poi_media_constraints.sql - -# Verify constraints applied -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT conname, contype -FROM pg_constraint -WHERE conrelid = 'poi_media'::regclass -ORDER BY conname; -" -``` - -**Expected Constraints:** -- `poi_media_caption_length_check` (CHECK) -- `poi_media_moderation_check` (CHECK) -- `poi_media_moderated_by_fkey` (FOREIGN KEY) -- `poi_media_submitted_by_fkey` (FOREIGN KEY) - -### Fix 3: Restart Service (if needed) - -```bash -# Restart to clear any caching issues -systemctl restart rootsofthevalley.org - -# Wait for startup -sleep 10 - -# Verify service is running -systemctl status rootsofthevalley.org --no-pager - -# Check logs for errors -journalctl -u rootsofthevalley.org --since "1 minute ago" --no-pager -``` - ---- - -## Verification Tests - -### Test 1: API Endpoints - -```bash -# Test media endpoint for POI #1 -curl -s https://rootsofthevalley.org/api/pois/1/media | jq - -# Should return: -# { -# "mosaic": [...], -# "all_media": [...], -# "total_count": N -# } - -# Test legacy thumbnail endpoint -curl -I https://rootsofthevalley.org/api/pois/1/thumbnail -# Should return: 200 OK (with image data) -``` - -### Test 2: Frontend UI - -1. Navigate to https://rootsofthevalley.org -2. Click any POI marker on the map -3. Sidebar should show: - - Mosaic display (Facebook-style grid) if multiple images - - Single image if only one image - - No broken image icons - -### Test 3: Database Queries - -```bash -# Check media for a specific POI -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT - id, - media_type, - role, - image_server_asset_id, - moderation_status -FROM poi_media -WHERE poi_id = 1; -" - -# Check moderation queue -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT COUNT(*) -FROM moderation_queue -WHERE content_type = 'photo'; -" -``` - ---- - -## Common Issues - -### Issue: "Image server not configured" - -**Symptoms:** Logs show `[ImageServer] Not configured - set IMAGE_SERVER_URL` - -**Fix:** -```bash -# Check systemd service file for IMAGE_SERVER_URL environment variable -systemctl cat rootsofthevalley.org | grep -i image - -# If missing, edit service file and add: -# Environment="IMAGE_SERVER_URL=http://10.89.1.100:8000" - -# Reload and restart -systemctl daemon-reload -systemctl restart rootsofthevalley.org -``` - -### Issue: Migration script fails with "ECONNREFUSED" - -**Symptoms:** `migrate-primary-images.js` can't connect to image server - -**Fix:** -```bash -# Test connectivity from container -podman exec rootsofthevalley.org curl -v http://10.89.1.100:8000/api/health - -# If connection refused, check: -# 1. Image server is running -# 2. Firewall rules allow 10.89.1.100:8000 -# 3. Network routing is correct -``` - -### Issue: Migration script creates duplicates - -**Symptoms:** Unique constraint violation on `idx_poi_media_unique_primary` - -**Fix:** The script checks for existing entries and skips them. If duplicates occur: -```bash -# Find duplicates -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -SELECT poi_id, COUNT(*) -FROM poi_media -WHERE role = 'primary' - AND moderation_status IN ('published', 'auto_approved') -GROUP BY poi_id -HAVING COUNT(*) > 1; -" - -# Manually clean up (keep oldest, delete newer) -podman exec rootsofthevalley.org psql -U postgres -d rotv -c " -DELETE FROM poi_media -WHERE id IN ( - SELECT id - FROM ( - SELECT id, poi_id, - ROW_NUMBER() OVER (PARTITION BY poi_id ORDER BY created_at ASC) as rn - FROM poi_media - WHERE role = 'primary' - AND moderation_status IN ('published', 'auto_approved') - ) sub - WHERE rn > 1 -); -" -``` - ---- - -## Rollback (if needed) - -If deployment is broken beyond repair: - -```bash -# Find backup file -ls -lh /root/backups/rotv_pre_multi_image_* - -# Restore database -podman exec -i rootsofthevalley.org psql -U postgres rotv < /root/backups/rotv_pre_multi_image_.sql - -# Revert to previous container image -podman images quay.io/crunchtools/rotv -podman tag quay.io/crunchtools/rotv: quay.io/crunchtools/rotv:latest - -# Restart service -systemctl restart rootsofthevalley.org -``` - ---- - -## Success Checklist - -- [ ] `poi_media` table exists -- [ ] `poi_media` has records (COUNT > 0) -- [ ] Primary images migrated (role='primary' count matches POIs with images) -- [ ] Migration 016 constraints applied -- [ ] Image server connectivity verified -- [ ] API endpoint `/api/pois/1/media` returns data -- [ ] Frontend displays mosaic/images correctly -- [ ] No errors in service logs -- [ ] Legacy thumbnail endpoint works - ---- - -## Contact - -**Issue:** PR #182 - Multi-Image POI Support -**PR Link:** https://github.com/crunchtools/rotv/pull/182 -**Deployment Runbook:** `.specify/specs/004-multi-image-poi/DEPLOYMENT_RUNBOOK.md` diff --git a/README_PRODUCTION.md b/README_PRODUCTION.md deleted file mode 100644 index c923282d..00000000 --- a/README_PRODUCTION.md +++ /dev/null @@ -1,368 +0,0 @@ -# Production Operations - Roots of The Valley - -**Quick Links:** [Deploy](#deployment) • [Troubleshoot](#troubleshooting) • [Incident Response](#incident-response) • [Monitoring](#monitoring) • [Rollback](#rollback) - ---- - -## 🚀 Deployment - -### Quick Deploy (No Migrations) -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com -podman pull quay.io/crunchtools/rotv:latest && systemctl restart rootsofthevalley.org -curl -sf https://rootsofthevalley.org/api/health && echo "✅ OK" -``` - -### Full Deployment Guide -See **[DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md)** for: -- Standard deployment process -- Deployment with migrations -- Rollback procedures -- Monitoring guidelines -- Common scenarios - -### Post-Deployment Verification -```bash -# Automated report (recommended) -bash scripts/post-deployment-report.sh - -# Or run smoke tests -gh workflow run smoke-test.yml -``` - ---- - -## 🔍 Troubleshooting - -### Quick Health Check (30 seconds) -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com "podman exec rootsofthevalley.org psql -U postgres -d rotv -c \"SELECT 'Total POIs:' as check, COUNT(*)::text FROM pois UNION ALL SELECT 'Media records:', COUNT(*)::text FROM poi_media;\"" -``` - -### Automated Diagnostics -```bash -# Run comprehensive diagnostics -ssh -p 22422 root@lotor.dc3.crunchtools.com -bash scripts/diagnose-production.sh -``` - -### Troubleshooting Resources - -| Issue | Resource | -|-------|----------| -| General issues | **[PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md)** | -| Quick fixes | **[PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md)** | -| Images not loading | **[PRODUCTION_INCIDENT_README.md](./PRODUCTION_INCIDENT_README.md)** | -| Understanding data flow | **[PROD_ISSUE_FLOWCHART.md](./PROD_ISSUE_FLOWCHART.md)** | -| Migration issues | Run `scripts/verify-migrations.sh` | - ---- - -## 🚨 Incident Response - -### Active Incident? Start Here - -**[PRODUCTION_INCIDENT_README.md](./PRODUCTION_INCIDENT_README.md)** - Choose your path: - -1. **Just Fix It (5 min)** - Copy-paste commands for immediate resolution -2. **Diagnose First (10 min)** - Run automated diagnostics then fix -3. **Understand First (20 min)** - Deep dive then fix - -### Incident Response Checklist - -- [ ] Identify symptoms (what's broken?) -- [ ] Check service status: `systemctl status rootsofthevalley.org` -- [ ] Review recent logs: `journalctl -u rootsofthevalley.org --since "1 hour ago"` -- [ ] Run diagnostics: `bash scripts/diagnose-production.sh` -- [ ] Determine severity (critical? major? minor?) -- [ ] Follow appropriate fix procedure -- [ ] Verify fix worked -- [ ] Document incident (use **[EXEC_SUMMARY.md](./EXEC_SUMMARY.md)** template) - -### Common Incidents - -#### Images Not Loading -```bash -# Diagnosis -podman exec rootsofthevalley.org psql -U postgres -d rotv -tAc "SELECT COUNT(*) FROM poi_media WHERE role='primary';" - -# Fix (if count is 0) -podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js -systemctl restart rootsofthevalley.org -``` - -See **[PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md)** for more quick fixes. - -#### Service Won't Start -```bash -# Check logs -journalctl -u rootsofthevalley.org --no-pager -n 50 - -# Common causes: -# - Database not ready -# - Port conflict -# - Migration failure -# - Configuration error - -# See DEPLOYMENT_GUIDE.md → Troubleshooting Deployments -``` - ---- - -## 📊 Monitoring - -### Real-Time Monitoring -```bash -# Watch logs -journalctl -u rootsofthevalley.org -f - -# Watch resource usage -podman stats rootsofthevalley.org -``` - -### Periodic Health Checks - -```bash -# Every 10 minutes (first hour after deployment) -curl -sf https://rootsofthevalley.org/api/health - -# Every 4 hours (first 24 hours) -bash scripts/post-deployment-report.sh - -# Weekly -gh workflow run smoke-test.yml -``` - -### Key Metrics to Monitor - -| Metric | Command | Healthy Value | -|--------|---------|---------------| -| Service status | `systemctl status rootsofthevalley.org` | active (running) | -| Error rate | `journalctl -u rootsofthevalley.org --since "1 hour ago" \| grep -i error \| wc -l` | < 10 per hour | -| Response time | `time curl -s https://rootsofthevalley.org/api/pois/1/media > /dev/null` | < 1 second | -| Database size | `podman exec rootsofthevalley.org psql -U postgres -d rotv -c "SELECT pg_size_pretty(pg_database_size('rotv'));"` | Grows steadily | -| Media count | `podman exec rootsofthevalley.org psql -U postgres -d rotv -tAc "SELECT COUNT(*) FROM poi_media;"` | Grows over time | - ---- - -## ↩️ Rollback - -### Quick Rollback (Container Only) -```bash -# Revert to previous container (2 minutes) -ssh -p 22422 root@lotor.dc3.crunchtools.com -podman images quay.io/crunchtools/rotv -podman tag quay.io/crunchtools/rotv: quay.io/crunchtools/rotv:latest -systemctl restart rootsofthevalley.org -``` - -### Full Rollback (Container + Database) -```bash -# Restore everything (5 minutes) -ssh -p 22422 root@lotor.dc3.crunchtools.com -ls -lht /root/backups/rotv_* | head -5 -podman exec -i rootsofthevalley.org psql -U postgres rotv < /root/backups/rotv_TIMESTAMP.sql -podman tag quay.io/crunchtools/rotv: quay.io/crunchtools/rotv:latest -systemctl restart rootsofthevalley.org -``` - -See **[DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md#rollback-procedures)** for detailed rollback procedures. - ---- - -## 🛠️ Scripts Reference - -| Script | Purpose | Usage | -|--------|---------|-------| -| **diagnose-production.sh** | Automated diagnostics | `bash scripts/diagnose-production.sh` | -| **fix-production.sh** | Automated fix with backup | `bash scripts/fix-production.sh` | -| **verify-migrations.sh** | Verify migrations applied | `bash scripts/verify-migrations.sh` | -| **post-deployment-report.sh** | Generate deployment report | `bash scripts/post-deployment-report.sh` | - -All scripts should be run on production server after SSH. - ---- - -## 📚 Documentation Index - -### Deployment & Operations -- **[DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md)** - Complete deployment guide -- **[DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md)** - Post-deployment checklist - -### Troubleshooting -- **[PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md)** - Comprehensive troubleshooting -- **[PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md)** - Quick reference commands -- **[PROD_ISSUE_FLOWCHART.md](./PROD_ISSUE_FLOWCHART.md)** - Visual debugging guide - -### Incident Response -- **[PRODUCTION_INCIDENT_README.md](./PRODUCTION_INCIDENT_README.md)** - Incident response guide -- **[EXEC_SUMMARY.md](./EXEC_SUMMARY.md)** - Executive summary template - -### Development -- **[CLAUDE.md](./CLAUDE.md)** - Development guidelines -- **[docs/DEVELOPMENT_ARCHITECTURE.md](./docs/DEVELOPMENT_ARCHITECTURE.md)** - System architecture -- **[.specify/memory/constitution.md](./.specify/memory/constitution.md)** - Project constitution - ---- - -## 🔗 Quick Reference Links - -### Production Environment -- **URL:** https://rootsofthevalley.org -- **Server:** lotor.dc3.crunchtools.com:22422 -- **Container:** rootsofthevalley.org -- **Registry:** quay.io/crunchtools/rotv:latest -- **Database:** PostgreSQL 17 (rotv) - -### SSH Access -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com -``` - -### GitHub Actions -```bash -# Trigger smoke tests -gh workflow run smoke-test.yml - -# Check recent builds -gh run list --workflow=build.yml --limit 5 - -# Monitor running workflow -gh run watch -``` - -### Key API Endpoints -- Health: https://rootsofthevalley.org/api/health -- POIs: https://rootsofthevalley.org/api/pois -- Media: https://rootsofthevalley.org/api/pois/{id}/media -- Thumbnails: https://rootsofthevalley.org/api/pois/{id}/thumbnail - ---- - -## 📞 Support & Escalation - -### When to Escalate -- Service down for > 15 minutes -- Data loss detected -- Security incident -- Unable to resolve using troubleshooting guides - -### Escalation Path -1. Review all troubleshooting documents -2. Run automated diagnostics -3. Attempt rollback if appropriate -4. Document incident details -5. Create GitHub issue with details -6. Contact deployment owner - -### Contact -- **GitHub Issues:** https://github.com/crunchtools/rotv/issues -- **Deployment Owner:** Scott McCarty (@fatherlinux) - ---- - -## 🎓 Learning Resources - -### Recent Incidents & Lessons Learned - -#### PR #182: Image Loading Failure (2026-04-04) -**Issue:** Images not loading after deployment -**Cause:** Migration script not executed -**Resolution:** Run `migrate-primary-images.js` script -**Lessons:** -- Always verify migrations after deployment -- Use post-deployment verification checklist -- Automate smoke tests in CI/CD - -**Full Documentation:** -- **[PRODUCTION_INCIDENT_README.md](./PRODUCTION_INCIDENT_README.md)** -- **[EXEC_SUMMARY.md](./EXEC_SUMMARY.md)** - ---- - -## 🔄 Continuous Improvement - -### After Every Deployment -- [ ] Generate post-deployment report -- [ ] Review any issues encountered -- [ ] Update runbooks if needed -- [ ] Add to lessons learned - -### After Every Incident -- [ ] Document incident details -- [ ] Perform root cause analysis -- [ ] Update troubleshooting guides -- [ ] Implement prevention measures -- [ ] Add to monitoring/alerts - -### Quarterly Review -- [ ] Review all incidents -- [ ] Identify patterns -- [ ] Update automation -- [ ] Improve monitoring -- [ ] Train team on new procedures - ---- - -## 📋 Checklists - -### Pre-Deployment -- [ ] All tests passing -- [ ] Security scans clean -- [ ] Migrations identified and ready -- [ ] Backup strategy confirmed -- [ ] Rollback procedure understood - -### During Deployment -- [ ] Backup created -- [ ] Migrations applied -- [ ] Migrations verified -- [ ] Service restarted -- [ ] Service healthy - -### Post-Deployment -- [ ] Health checks passing -- [ ] Feature tests passed -- [ ] Smoke tests run -- [ ] Report generated -- [ ] Monitoring active - -See **[DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md)** for detailed checklist. - ---- - -## 🆘 Emergency Contacts & Quick Commands - -### Immediate Response Commands - -```bash -# Stop the service (emergency only) -systemctl stop rootsofthevalley.org - -# Check if service is running -systemctl status rootsofthevalley.org - -# View last 50 log lines -journalctl -u rootsofthevalley.org --no-pager -n 50 - -# Check database connection -podman exec rootsofthevalley.org psql -U postgres -d rotv -c "SELECT 1;" - -# Check disk space -df -h - -# Check container resource usage -podman stats --no-stream rootsofthevalley.org -``` - -### Emergency Rollback -```bash -# One-liner rollback to previous container -podman tag quay.io/crunchtools/rotv:$(podman images quay.io/crunchtools/rotv --format "{{.Tag}}" | grep -v latest | head -1) quay.io/crunchtools/rotv:latest && systemctl restart rootsofthevalley.org -``` - ---- - -**Last Updated:** 2026-04-04 -**Version:** 1.0 -**Maintainer:** Scott McCarty (@fatherlinux) diff --git a/TROUBLESHOOTING_PACKAGE_INDEX.md b/TROUBLESHOOTING_PACKAGE_INDEX.md deleted file mode 100644 index 18d084a8..00000000 --- a/TROUBLESHOOTING_PACKAGE_INDEX.md +++ /dev/null @@ -1,466 +0,0 @@ -# Troubleshooting Package - Complete Index - -**Created:** 2026-04-04 -**Purpose:** Production issue response for PR #182 image loading failure -**Status:** Complete and ready for use - ---- - -## 📦 Package Contents - -This comprehensive troubleshooting package provides everything needed to diagnose, fix, and prevent production issues. - -### 🎯 Start Here - -**[README_PRODUCTION.md](./README_PRODUCTION.md)** - Main production operations guide -- Quick reference for all production tasks -- Links to all resources -- Emergency commands -- Quick health checks - -### 📋 Documentation Structure - -``` -Production Operations -├── README_PRODUCTION.md .................. Main entry point -├── DEPLOYMENT_GUIDE.md ................... Complete deployment guide -├── DEPLOYMENT_VERIFICATION_CHECKLIST.md .. Post-deployment checklist -│ -Incident Response -├── PRODUCTION_INCIDENT_README.md ......... Incident response guide -├── EXEC_SUMMARY.md ....................... Executive summary template -│ -Troubleshooting -├── PROD_TROUBLESHOOT.md .................. Comprehensive troubleshooting -├── PROD_FIX_QUICKREF.md .................. Quick reference commands -├── PROD_ISSUE_FLOWCHART.md ............... Visual debugging guide -│ -Scripts -├── scripts/diagnose-production.sh ........ Automated diagnostics -├── scripts/fix-production.sh ............. Automated fix with backup -├── scripts/verify-migrations.sh .......... Migration verification -└── scripts/post-deployment-report.sh ..... Deployment report generator - -Automation -└── .github/workflows/smoke-test.yml ...... Smoke tests (GitHub Actions) -``` - ---- - -## 🚀 Quick Start Guides - -### Scenario 1: Images Not Loading (PR #182) -**Time: 5 minutes** - -1. **Diagnose:** - ```bash - ssh -p 22422 root@lotor.dc3.crunchtools.com - podman exec rootsofthevalley.org psql -U postgres -d rotv -tAc \ - "SELECT COUNT(*) FROM poi_media WHERE role='primary';" - # If 0, migration script wasn't run - ``` - -2. **Fix:** - ```bash - podman exec rootsofthevalley.org node /app/scripts/migrate-primary-images.js - systemctl restart rootsofthevalley.org - ``` - -3. **Verify:** - ```bash - curl -s https://rootsofthevalley.org/api/pois/1/media | jq '.total_count' - # Should return > 0 - ``` - -**Resources:** [PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md) - -### Scenario 2: Full Health Check -**Time: 2 minutes** - -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com -bash scripts/diagnose-production.sh -``` - -**Resources:** [scripts/diagnose-production.sh](./scripts/diagnose-production.sh) - -### Scenario 3: Post-Deployment Verification -**Time: 3 minutes** - -```bash -ssh -p 22422 root@lotor.dc3.crunchtools.com -bash scripts/post-deployment-report.sh -``` - -**Resources:** [DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md) - ---- - -## 📚 Documentation by Use Case - -### For Deployers -**Primary:** [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md) -- Standard deployment process -- Deployment with migrations -- Rollback procedures -- Common scenarios - -**Secondary:** -- [DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md) -- [scripts/post-deployment-report.sh](./scripts/post-deployment-report.sh) - -### For On-Call Engineers -**Primary:** [README_PRODUCTION.md](./README_PRODUCTION.md) -- Quick reference -- Emergency commands -- Common issues - -**Secondary:** -- [PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md) -- [scripts/diagnose-production.sh](./scripts/diagnose-production.sh) - -### For Incident Response -**Primary:** [PRODUCTION_INCIDENT_README.md](./PRODUCTION_INCIDENT_README.md) -- Three resolution paths (fast/medium/thorough) -- Incident flow -- Testing checklist - -**Secondary:** -- [EXEC_SUMMARY.md](./EXEC_SUMMARY.md) - For stakeholder communication -- [PROD_ISSUE_FLOWCHART.md](./PROD_ISSUE_FLOWCHART.md) - Visual understanding - -### For Troubleshooting -**Primary:** [PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md) -- Diagnostic steps -- Fix procedures -- Common errors - -**Secondary:** -- [PROD_ISSUE_FLOWCHART.md](./PROD_ISSUE_FLOWCHART.md) - Data flow diagrams -- [scripts/diagnose-production.sh](./scripts/diagnose-production.sh) - Automated diagnostics - -### For Executives/Stakeholders -**Primary:** [EXEC_SUMMARY.md](./EXEC_SUMMARY.md) -- What happened -- Impact assessment -- Resolution time -- Prevention measures - -**Secondary:** -- [PRODUCTION_INCIDENT_README.md](./PRODUCTION_INCIDENT_README.md) - Incident details - ---- - -## 🛠️ Scripts Reference - -### Diagnostic Scripts - -#### diagnose-production.sh -**Purpose:** Comprehensive automated diagnostics -**Time:** 30 seconds -**Output:** Pass/fail checks with recommendations - -```bash -bash scripts/diagnose-production.sh -``` - -**Checks:** -- Container status -- Database tables -- Record counts -- Migrations applied -- API endpoints -- Error rates - -#### verify-migrations.sh -**Purpose:** Verify all database migrations -**Time:** 20 seconds -**Output:** Detailed migration status - -```bash -bash scripts/verify-migrations.sh -``` - -**Checks:** -- Table existence -- Column schemas -- Indexes -- Constraints -- Data population -- Data integrity - -### Fix Scripts - -#### fix-production.sh -**Purpose:** Automated fix with backup -**Time:** 5 minutes -**Interactive:** Yes (asks for confirmation) - -```bash -bash scripts/fix-production.sh -``` - -**Actions:** -- Creates database backup -- Applies migration 016 -- Runs primary image migration -- Restarts service -- Verifies fix - -### Reporting Scripts - -#### post-deployment-report.sh -**Purpose:** Generate deployment health report -**Time:** 10 seconds -**Output:** Markdown report file - -```bash -bash scripts/post-deployment-report.sh -``` - -**Includes:** -- Service status -- Database health -- API endpoint status -- Migration status -- Recent errors -- Recommendations - ---- - -## 🔄 GitHub Actions Workflows - -### smoke-test.yml -**Trigger:** Manual (`gh workflow run smoke-test.yml`) -**Purpose:** Post-deployment smoke tests -**Time:** 2-3 minutes - -**Tests:** -1. Health endpoint -2. POI list endpoint -3. Media endpoint (PR #182 critical) -4. Thumbnail endpoint -5. Asset proxy SSRF protection -6. Auth status endpoint -7. Frontend loads -8. Database connectivity -9. Response time check - -**Usage:** -```bash -# Trigger from local machine -gh workflow run smoke-test.yml - -# Monitor progress -gh run watch - -# View results -gh run view -``` - ---- - -## 📖 Reading Paths - -### Path 1: "Just Fix It" (5 minutes) -For experienced ops engineers who need immediate resolution: - -1. [PROD_FIX_QUICKREF.md](./PROD_FIX_QUICKREF.md) → Copy-paste commands -2. Execute fix -3. Verify with quick health check - -### Path 2: "Diagnose Then Fix" (10 minutes) -For methodical troubleshooting: - -1. Run `scripts/diagnose-production.sh` -2. Review output -3. Run `scripts/fix-production.sh` (if needed) -4. Verify with `scripts/post-deployment-report.sh` - -### Path 3: "Understand First" (30 minutes) -For learning and prevention: - -1. [EXEC_SUMMARY.md](./EXEC_SUMMARY.md) - What happened -2. [PROD_ISSUE_FLOWCHART.md](./PROD_ISSUE_FLOWCHART.md) - How it works -3. [PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md) - Detailed diagnosis -4. [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md) - Prevent recurrence -5. Apply fix -6. Review [DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md) - -### Path 4: "New to Production Ops" (1 hour) -For onboarding: - -1. [README_PRODUCTION.md](./README_PRODUCTION.md) - Overview -2. [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md) - How to deploy -3. [DEPLOYMENT_VERIFICATION_CHECKLIST.md](./DEPLOYMENT_VERIFICATION_CHECKLIST.md) - Checklist -4. [PROD_TROUBLESHOOT.md](./PROD_TROUBLESHOOT.md) - Common issues -5. Practice: Run `scripts/diagnose-production.sh` - ---- - -## 🎯 Key Features - -### Automation -- ✅ Automated diagnostics (diagnose-production.sh) -- ✅ Automated fix with backup (fix-production.sh) -- ✅ Migration verification (verify-migrations.sh) -- ✅ Post-deployment reporting (post-deployment-report.sh) -- ✅ Smoke tests (GitHub Actions) - -### Documentation -- ✅ Multiple reading paths (fast/thorough) -- ✅ Visual diagrams (flowcharts, data flow) -- ✅ Copy-paste commands (no guessing) -- ✅ Comprehensive troubleshooting guide -- ✅ Executive summaries (stakeholder communication) - -### Prevention -- ✅ Deployment verification checklist -- ✅ Post-deployment smoke tests -- ✅ Migration verification -- ✅ Lessons learned documented -- ✅ Rollback procedures - ---- - -## 📊 Package Statistics - -- **Total Documents:** 11 -- **Total Scripts:** 4 -- **Total Workflows:** 1 -- **Total Lines of Documentation:** ~4,500 -- **Total Lines of Code (scripts):** ~800 -- **Copy-Paste Commands:** 50+ -- **Diagnostic Checks:** 30+ -- **Coverage:** Complete incident lifecycle - ---- - -## 🔗 Cross-References - -### From Issue to Resolution - -``` -User Reports Issue - ↓ -README_PRODUCTION.md (start here) - ↓ -Choose Path: - ├─ Fast → PROD_FIX_QUICKREF.md - ├─ Diagnostic → scripts/diagnose-production.sh - └─ Deep Dive → PROD_TROUBLESHOOT.md - ↓ -Apply Fix - ├─ Automated → scripts/fix-production.sh - └─ Manual → PROD_FIX_QUICKREF.md - ↓ -Verify Fix - ├─ Quick → curl health check - └─ Thorough → scripts/post-deployment-report.sh - ↓ -Document Incident - └─ EXEC_SUMMARY.md template - ↓ -Prevent Recurrence - └─ DEPLOYMENT_VERIFICATION_CHECKLIST.md -``` - -### Deployment Flow - -``` -Merge PR - ↓ -GitHub Actions Build - ↓ -DEPLOYMENT_GUIDE.md - ├─ Standard deployment - └─ Migration deployment - ↓ -Apply Changes - ├─ Container update - └─ Database migrations - ↓ -Verify Deployment - ├─ scripts/post-deployment-report.sh - ├─ DEPLOYMENT_VERIFICATION_CHECKLIST.md - └─ smoke-test.yml (GitHub Actions) - ↓ -Monitor - └─ README_PRODUCTION.md → Monitoring section -``` - ---- - -## 🎓 Learning Outcomes - -After using this package, operators will be able to: - -1. **Diagnose** production issues in < 2 minutes -2. **Fix** common issues in < 5 minutes -3. **Verify** deployments systematically -4. **Rollback** safely when needed -5. **Document** incidents for stakeholders -6. **Prevent** issues through checklists -7. **Automate** common tasks -8. **Communicate** effectively during incidents - ---- - -## 🔄 Maintenance - -### When to Update This Package - -- After every production incident (add to lessons learned) -- After major feature deployments (update procedures) -- Quarterly review (refresh and improve) -- When automation improves (update scripts) - -### How to Update - -1. Document new issues in PROD_TROUBLESHOOT.md -2. Add commands to PROD_FIX_QUICKREF.md -3. Update scripts with new checks -4. Add to DEPLOYMENT_VERIFICATION_CHECKLIST.md -5. Update this index - ---- - -## 📞 Feedback & Improvement - -### Report Issues -- GitHub Issues: https://github.com/crunchtools/rotv/issues -- Tag issues with `documentation` or `operations` - -### Suggest Improvements -- Better automation -- Missing scenarios -- Unclear documentation -- New monitoring needs - ---- - -## ✅ Quality Checklist - -This package includes: - -- [x] Quick start guides (< 5 minutes to resolution) -- [x] Comprehensive troubleshooting (all scenarios covered) -- [x] Automated diagnostics (no manual checks needed) -- [x] Automated fixes (safe with backups) -- [x] Visual diagrams (data flow, flowcharts) -- [x] Copy-paste commands (no guessing) -- [x] Rollback procedures (tested and safe) -- [x] Verification checklists (prevent issues) -- [x] Incident templates (stakeholder communication) -- [x] Prevention measures (lessons learned) -- [x] Cross-references (easy navigation) -- [x] Multiple reading paths (all skill levels) - ---- - -**Created By:** Claude Sonnet 4.5 (AI Assistant) -**Reviewed By:** Pending -**Version:** 1.0 -**Last Updated:** 2026-04-04 - -**Next Review:** After next production incident or quarterly diff --git a/backend/config/passport.js b/backend/config/passport.js index 45a01461..970022f8 100644 --- a/backend/config/passport.js +++ b/backend/config/passport.js @@ -81,17 +81,36 @@ export function configurePassport(pool) { return insertResult.rows[0]; } - // Google OAuth Strategy - request drive.file scope for all users - // (non-admins won't use it, but it simplifies the flow) - // Note: accessType and prompt are set in auth.js route, not here + // Google OAuth Strategy (dual-strategy approach for conditional Drive access) + // Standard strategy - all users get basic profile + email scopes only + // Admin users are auto-redirected to upgrade flow if they lack Drive credentials if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { - passport.use(new GoogleStrategy({ + // Standard strategy - basic scopes for all users (no Drive access) + passport.use('google', new GoogleStrategy({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL || '/auth/google/callback', + scope: ['profile', 'email'] + }, async (accessToken, refreshToken, profile, done) => { + try { + // Don't store credentials for standard login (admin will get them via upgrade flow) + const user = await findOrCreateUser('google', profile, null); + done(null, user); + } catch (error) { + done(error); + } + })); + + // Upgrade strategy - Drive scope for admin only (incremental authorization) + // Uses same callback URL as standard strategy to avoid multiple OAuth app configurations + passport.use('google-upgrade', new GoogleStrategy({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: process.env.GOOGLE_CALLBACK_URL || '/auth/google/callback', scope: ['profile', 'email', 'https://www.googleapis.com/auth/drive.file'] }, async (accessToken, refreshToken, profile, done) => { try { + // Store Drive credentials for admin const credentials = { access_token: accessToken, refresh_token: refreshToken @@ -102,7 +121,8 @@ export function configurePassport(pool) { done(error); } })); - console.log('Google OAuth strategy configured'); + + console.log('Google OAuth strategies configured (standard + upgrade)'); } else { console.log('Google OAuth not configured (missing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET)'); } diff --git a/backend/migrations/017_increase_caption_length.sql b/backend/migrations/017_increase_caption_length.sql new file mode 100644 index 00000000..946b3191 --- /dev/null +++ b/backend/migrations/017_increase_caption_length.sql @@ -0,0 +1,11 @@ +-- Migration 017: Increase caption length limit +-- Created: 2026-04-04 +-- Description: Increase poi_media caption limit from 200 to 2000 characters +-- to allow for more descriptive captions + +-- Drop the existing 200-character constraint +ALTER TABLE poi_media DROP CONSTRAINT IF EXISTS poi_media_caption_length_check; + +-- Add new constraint with 2000-character limit +ALTER TABLE poi_media ADD CONSTRAINT poi_media_caption_length_check + CHECK (caption IS NULL OR length(caption) <= 2000); diff --git a/backend/routes/admin.js b/backend/routes/admin.js index 4888073e..a308ce52 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -3922,13 +3922,16 @@ export function createAdminRouter(pool, invalidateMosaicCache) { router.post('/moderation/save', isAdmin, async (req, res) => { try { const { type, id, edits } = req.body; + console.log('[Moderation Save] Request:', { type, id, edits }); if (!type || !id || !edits) { return res.status(400).json({ error: 'type, id, and edits are required' }); } await editAndPublish(pool, type, id, edits, req.user.id, { publish: false }); + console.log('[Moderation Save] Success'); res.json({ success: true }); } catch (error) { - console.error('Error saving edits:', error); + console.error('[Moderation Save] Error:', error.message); + console.error('[Moderation Save] Stack:', error.stack); res.status(500).json({ error: 'Failed to save edits' }); } }); diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 361e417c..19b39363 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -4,25 +4,64 @@ import passport from 'passport'; const router = express.Router(); const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:8080'; +const ADMIN_EMAIL = process.env.ADMIN_EMAIL || ''; -// Google OAuth - single flow for all users -// Admins get drive.file scope and credentials stored in database -// accessType: 'offline' requests a refresh token -// prompt: 'consent' forces consent screen to ensure we get refresh token +// Google OAuth - dual-strategy approach for conditional Drive access +// Standard route: all users authenticate with basic scopes (profile + email) +// Upgrade route: admin-only Drive scope via incremental authorization +// Auto-detection: admin users without Drive credentials are redirected to upgrade flow // Fix: Only register routes if strategy is configured (prevents "Unknown strategy" error) if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { - router.get('/google', passport.authenticate('google', { + // Standard Google OAuth (all users - basic scopes only) + router.get('/google', passport.authenticate('google')); + + // Drive scope upgrade (admin only - incremental authorization) + router.get('/google/upgrade', passport.authenticate('google-upgrade', { accessType: 'offline', - prompt: 'consent' + prompt: 'consent', + state: 'upgrade' // Pass state to identify upgrade flow in callback })); - router.get('/google/callback', - passport.authenticate('google', { failureRedirect: `${FRONTEND_URL}?auth=failed` }), - (req, res) => { - // Always redirect to View tab (default) after login + // Unified callback handler - handles both standard and upgrade flows + router.get('/google/callback', (req, res, next) => { + // Check if this is an upgrade callback (state=upgrade) + const isUpgrade = req.query.state === 'upgrade'; + const strategy = isUpgrade ? 'google-upgrade' : 'google'; + + passport.authenticate(strategy, { + failureRedirect: `${FRONTEND_URL}?auth=failed` + })(req, res, async () => { + // Handle upgrade flow - redirect to Sync Settings + if (isUpgrade) { + return res.redirect(`${FRONTEND_URL}/admin?auth=success&tab=sync`); + } + + // Handle standard flow - auto-detect admin without Drive credentials + const isAdmin = req.user.email?.toLowerCase() === ADMIN_EMAIL.toLowerCase(); + + // Parse credentials (handles both JSON string and object from pg driver) + let credentials = null; + if (req.user.oauth_credentials) { + try { + credentials = typeof req.user.oauth_credentials === 'string' + ? JSON.parse(req.user.oauth_credentials) + : req.user.oauth_credentials; + } catch (err) { + console.error('Failed to parse oauth_credentials:', err); + credentials = null; + } + } + const hasCredentials = credentials && credentials.access_token; + + if (isAdmin && !hasCredentials) { + // Redirect admin to upgrade flow for Drive access + return res.redirect('/auth/google/upgrade'); + } + + // Standard success redirect res.redirect(`${FRONTEND_URL}?auth=success`); - } - ); + }); + }); } else { // Return helpful error when OAuth not configured router.get('/google', (req, res) => { diff --git a/backend/server.js b/backend/server.js index 045badaf..f7c72db2 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1008,14 +1008,17 @@ app.get('/api/pois/:id/thumbnail', async (req, res) => { app.get('/api/pois/:id/media', async (req, res) => { try { const { id } = req.params; + const userId = req.user?.id || null; // Optional authentication - // Check cache first (5min TTL reduces DB load) - const cached = getMosaicFromCache(id); - if (cached) { - return res.json(cached); + // Skip cache if authenticated (user might have pending uploads) + if (!userId) { + const cached = getMosaicFromCache(id); + if (cached) { + return res.json(cached); + } } - // Query approved media from poi_media table + // Query media: published for everyone, pending only for uploader const result = await pool.query(` SELECT id, @@ -1026,15 +1029,20 @@ app.get('/api/pois/:id/media', async (req, res) => { sort_order, likes_count, caption, - created_at + created_at, + moderation_status, + submitted_by FROM poi_media WHERE poi_id = $1 - AND moderation_status IN ('published', 'auto_approved') + AND ( + moderation_status IN ('published', 'auto_approved') + OR (moderation_status = 'pending' AND submitted_by = $2) + ) ORDER BY CASE WHEN role = 'primary' THEN 0 ELSE 1 END, likes_count DESC, created_at DESC - `, [id]); + `, [id, userId]); const allMedia = []; @@ -1046,7 +1054,9 @@ app.get('/api/pois/:id/media', async (req, res) => { role: media.role, likes_count: media.likes_count, caption: media.caption, - created_at: media.created_at + created_at: media.created_at, + moderation_status: media.moderation_status, + uploaded_by_user: userId && media.submitted_by === userId }; if (media.media_type === 'youtube') { @@ -1075,8 +1085,10 @@ app.get('/api/pois/:id/media', async (req, res) => { total_count: allMedia.length }; - // Cache the result for 5 minutes - setMosaicCache(id, response); + // Only cache for anonymous users (authenticated users see their pending uploads) + if (!userId) { + setMosaicCache(id, response); + } res.json(response); } catch (error) { @@ -1147,18 +1159,26 @@ app.post('/api/pois/:id/media', isAuthenticated, upload.single('file'), async (r .substring(0, 255); // Limit length // Upload to image server - const uploadFn = media_type === 'image' - ? imageServerClient.uploadImage - : imageServerClient.uploadVideo; - - const uploadResult = await uploadFn.call( - imageServerClient, - req.file.buffer, - parseInt(id), - 'gallery', - sanitizedFilename, - req.file.mimetype - ); + let uploadResult; + if (media_type === 'image') { + // uploadImage(imageBuffer, poiId, role, filename, mimeType, options) + uploadResult = await imageServerClient.uploadImage( + req.file.buffer, + parseInt(id), + 'gallery', + sanitizedFilename, + req.file.mimetype + ); + } else { + // uploadVideo(videoBuffer, poiId, filename, mimeType, role) + uploadResult = await imageServerClient.uploadVideo( + req.file.buffer, + parseInt(id), + sanitizedFilename, + req.file.mimetype, + 'gallery' + ); + } if (!uploadResult.success) { return res.status(500).json({ error: 'Failed to upload: ' + uploadResult.error }); @@ -1167,11 +1187,11 @@ app.post('/api/pois/:id/media', isAuthenticated, upload.single('file'), async (r assetId = uploadResult.assetId; } - // Determine moderation status - const isMediaAdminUser = user.role === 'media_admin' || user.role === 'admin'; - const moderationStatus = isMediaAdminUser ? 'published' : 'pending'; - const moderatedAt = isMediaAdminUser ? new Date() : null; - const moderatedBy = isMediaAdminUser ? user.id : null; + // All uploads via this interface go to moderation queue + // (Admin panel uploads can still bypass queue) + const moderationStatus = 'pending'; + const moderatedAt = null; + const moderatedBy = null; // Set during approval, not upload // Create poi_media record const insertResult = await pool.query(` @@ -1203,9 +1223,8 @@ app.post('/api/pois/:id/media', isAuthenticated, upload.single('file'), async (r const mediaId = insertResult.rows[0].id; - const message = isMediaAdminUser - ? 'Media uploaded and published' - : 'Media submitted for review'; + // All uploads go to moderation queue + const message = 'Media submitted for review'; // Invalidate mosaic cache for this POI (new media uploaded) invalidateMosaicCache(id); @@ -1222,6 +1241,165 @@ app.post('/api/pois/:id/media', isAuthenticated, upload.single('file'), async (r } }); +/** + * DELETE /api/pois/:poiId/media/:mediaId + * Delete media (only allowed for uploader or admin) + */ +app.delete('/api/pois/:poiId/media/:mediaId', isAuthenticated, async (req, res) => { + try { + const { poiId, mediaId } = req.params; + const user = req.user; + + // Check if media exists and get ownership info + const mediaResult = await pool.query( + 'SELECT submitted_by, image_server_asset_id FROM poi_media WHERE id = $1 AND poi_id = $2', + [mediaId, poiId] + ); + + if (mediaResult.rows.length === 0) { + return res.status(404).json({ error: 'Media not found' }); + } + + const media = mediaResult.rows[0]; + + // Check permission: user must be the uploader or an admin + const isOwner = media.submitted_by === user.id; + const isAdmin = user.role === 'admin' || user.role === 'media_admin'; + + if (!isOwner && !isAdmin) { + return res.status(403).json({ error: 'You can only delete your own media' }); + } + + // Transaction: delete media and update POI flag atomically + await pool.query('BEGIN'); + + // Delete from database + await pool.query('DELETE FROM poi_media WHERE id = $1', [mediaId]); + + // Update POI's has_primary_image flag based on remaining media + const remainingPrimary = await pool.query( + `SELECT id FROM poi_media + WHERE poi_id = $1 + AND role = 'primary' + AND moderation_status IN ('published', 'auto_approved') + LIMIT 1`, + [poiId] + ); + + await pool.query( + 'UPDATE pois SET has_primary_image = $1 WHERE id = $2', + [remainingPrimary.rows.length > 0, poiId] + ); + + await pool.query('COMMIT'); + + // Delete from image server (if it's an image/video, not YouTube) + // NOTE: Eventual consistency - DB transaction already committed + // If image server delete fails, orphaned assets logged for cleanup (see #186) + let imageServerDeleted = true; + if (media.image_server_asset_id) { + try { + await imageServerClient.deleteAsset(media.image_server_asset_id); + } catch (err) { + console.error('Failed to delete asset from image server:', err); + console.error('Orphaned asset (manual cleanup required):', media.image_server_asset_id); + imageServerDeleted = false; + } + } + + // Invalidate mosaic cache + invalidateMosaicCache(poiId); + + // Return honest status about partial success + if (!imageServerDeleted) { + return res.status(202).json({ + success: true, + warning: 'Media deleted from database, image cleanup pending', + message: 'Media deleted' + }); + } + + res.json({ success: true, message: 'Media deleted' }); + } catch (error) { + await pool.query('ROLLBACK'); + console.error('Error deleting media:', error); + res.status(500).json({ error: 'Failed to delete media' }); + } +}); + +/** + * PATCH /api/pois/:poiId/media/:mediaId/set-primary + * Set media as primary (admins only) + */ +app.patch('/api/pois/:poiId/media/:mediaId/set-primary', isAuthenticated, async (req, res) => { + try { + const { poiId, mediaId } = req.params; + const user = req.user; + + // Check admin permission + const isAdmin = user.role === 'admin' || user.role === 'media_admin'; + if (!isAdmin) { + return res.status(403).json({ error: 'Only admins can set primary images' }); + } + + // Check if media exists and is published + const mediaResult = await pool.query( + 'SELECT id, role, moderation_status FROM poi_media WHERE id = $1 AND poi_id = $2', + [mediaId, poiId] + ); + + if (mediaResult.rows.length === 0) { + return res.status(404).json({ error: 'Media not found' }); + } + + const media = mediaResult.rows[0]; + + // Only published/auto-approved media can be primary + if (!['published', 'auto_approved'].includes(media.moderation_status)) { + return res.status(400).json({ error: 'Only approved media can be set as primary' }); + } + + // Already primary + if (media.role === 'primary') { + return res.json({ success: true, message: 'Already primary' }); + } + + // Transaction: demote old primary to gallery, promote new to primary + await pool.query('BEGIN'); + + // Demote old primary to gallery + await pool.query( + `UPDATE poi_media SET role = 'gallery' + WHERE poi_id = $1 AND role = 'primary' + AND moderation_status IN ('published', 'auto_approved')`, + [poiId] + ); + + // Promote new media to primary + await pool.query( + `UPDATE poi_media SET role = 'primary' WHERE id = $1`, + [mediaId] + ); + + // Update POI's has_primary_image flag + await pool.query( + 'UPDATE pois SET has_primary_image = true WHERE id = $1', + [poiId] + ); + + await pool.query('COMMIT'); + + // Invalidate mosaic cache + invalidateMosaicCache(poiId); + + res.json({ success: true, message: 'Primary image updated' }); + } catch (error) { + await pool.query('ROLLBACK'); + console.error('Error setting primary media:', error); + res.status(500).json({ error: 'Failed to set primary media' }); + } +}); + /** * GET /api/assets/:assetId/thumbnail * Proxy thumbnail from image server diff --git a/backend/services/moderationService.js b/backend/services/moderationService.js index b016fdf3..ed239f30 100644 --- a/backend/services/moderationService.js +++ b/backend/services/moderationService.js @@ -489,6 +489,8 @@ export async function editAndPublish(pool, contentType, contentId, edits, adminU : contentType === 'event' ? EDITABLE_EVENT : EDITABLE_PHOTO; const table = TABLE_MAP[contentType]; + console.log('[editAndPublish]', { contentType, contentId, edits, table, allowedFields }); + const setClauses = []; const values = [contentId]; let idx = 2; @@ -503,8 +505,8 @@ export async function editAndPublish(pool, contentType, contentId, edits, adminU } } - // When admin sets publication_date, mark confidence as 'exact' - if (edits.publication_date) { + // When admin sets publication_date, mark confidence as 'exact' (only for news/events, not photos) + if (edits.publication_date && contentType !== 'photo') { setClauses.push(`date_confidence = 'exact'`); } @@ -515,6 +517,7 @@ export async function editAndPublish(pool, contentType, contentId, edits, adminU } if (setClauses.length === 0) return; + console.log('[editAndPublish] SQL:', `UPDATE ${table} SET ${setClauses.join(', ')} WHERE id = $1`, values); await pool.query(`UPDATE ${table} SET ${setClauses.join(', ')} WHERE id = $1`, values); } @@ -931,7 +934,8 @@ export async function getQueue(pool, { page = 1, limit = 20, contentType = null, n.submitted_by, n.moderated_by, n.moderated_at, n.created_at, n.source_url, n.content_source, n.publication_date, n.date_confidence, NULL::TIMESTAMPTZ AS start_date, NULL::TIMESTAMPTZ AS end_date, - COUNT(u.id)::int AS additional_url_count + COUNT(u.id)::int AS additional_url_count, + NULL::VARCHAR AS media_type, NULL::VARCHAR AS image_server_asset_id, NULL::VARCHAR AS role FROM poi_news n LEFT JOIN poi_news_urls u ON u.news_id = n.id WHERE n.moderation_status = ANY($1) @@ -942,19 +946,26 @@ export async function getQueue(pool, { page = 1, limit = 20, contentType = null, e.submitted_by, e.moderated_by, e.moderated_at, e.created_at, e.source_url, e.content_source, e.publication_date, e.date_confidence, e.start_date, e.end_date, - COUNT(u.id)::int AS additional_url_count + COUNT(u.id)::int AS additional_url_count, + NULL::VARCHAR AS media_type, NULL::VARCHAR AS image_server_asset_id, NULL::VARCHAR AS role FROM poi_events e LEFT JOIN poi_event_urls u ON u.event_id = e.id WHERE e.moderation_status = ANY($1) GROUP BY e.id UNION ALL - SELECT id, 'photo' AS content_type, poi_id, original_filename AS title, caption AS description, + SELECT id, 'photo' AS content_type, poi_id, + CASE + WHEN media_type = 'youtube' THEN youtube_url + ELSE CONCAT(media_type, ' #', id) + END AS title, + caption AS description, moderation_status, confidence_score, ai_reasoning, NULL AS ai_issues, - submitted_by, moderated_by, moderated_at, created_at, NULL AS source_url, + submitted_by, moderated_by, moderated_at, created_at, youtube_url AS source_url, NULL AS content_source, NULL::DATE AS publication_date, NULL::VARCHAR AS date_confidence, NULL::TIMESTAMPTZ AS start_date, NULL::TIMESTAMPTZ AS end_date, - 0 AS additional_url_count - FROM photo_submissions WHERE moderation_status = ANY($1)`; + 0 AS additional_url_count, + media_type, image_server_asset_id, role + FROM poi_media WHERE moderation_status = ANY($1)`; const filters = []; const params = [statusList]; @@ -992,7 +1003,15 @@ export async function getQueue(pool, { page = 1, limit = 20, contentType = null, } export async function getPendingCount(pool) { - const countRow = await pool.query(`SELECT COUNT(*) FROM moderation_queue`); + const countRow = await pool.query(` + SELECT COUNT(*) FROM ( + SELECT id FROM poi_news WHERE moderation_status = 'pending' + UNION ALL + SELECT id FROM poi_events WHERE moderation_status = 'pending' + UNION ALL + SELECT id FROM poi_media WHERE moderation_status = 'pending' + ) AS pending_items + `); return parseInt(countRow.rows[0].count); } @@ -1004,7 +1023,7 @@ export async function getItemDetail(pool, contentType, contentId) { event: `SELECT e.*, p.name as poi_name, COALESCE(json_agg(json_build_object('id', u.id, 'url', u.url, 'source_name', u.source_name)) FILTER (WHERE u.id IS NOT NULL), '[]'::json) AS additional_urls FROM poi_events e LEFT JOIN pois p ON e.poi_id = p.id LEFT JOIN poi_event_urls u ON u.event_id = e.id WHERE e.id = $1 GROUP BY e.id, p.name`, - photo: `SELECT ps.*, p.name as poi_name FROM photo_submissions ps LEFT JOIN pois p ON ps.poi_id = p.id WHERE ps.id = $1` + photo: `SELECT pm.*, p.name as poi_name FROM poi_media pm LEFT JOIN pois p ON pm.poi_id = p.id WHERE pm.id = $1` }; const sql = queryMap[contentType]; diff --git a/frontend/src/App.css b/frontend/src/App.css index bb31e2c0..43e7223f 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1057,6 +1057,13 @@ body { } /* Sidebar Image */ +/* Multi-media section (mosaic or single image) */ +.sidebar-media-section { + width: 100%; + flex-shrink: 0; + background: #f5f5f5; +} + .sidebar-image { position: relative; width: 100%; @@ -1083,6 +1090,42 @@ body { padding: 0.25rem; } +/* No media state - empty placeholder with upload button */ +.sidebar-no-media { + position: relative; + width: 100%; + height: 200px; + min-height: 200px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + background: #f5f5f5; +} + +.btn-add-first-media { + background: #28a745; + color: white; + border: none; + padding: 12px 24px; + border-radius: 6px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.btn-add-first-media:hover { + background: #218838; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.btn-add-first-media:active { + transform: translateY(0); +} + .image-placeholder { width: 100%; height: 100%; @@ -6479,6 +6522,33 @@ body { font-size: 0.9rem; } +.drive-access-prompt { + background: #fff3cd; + border: 1px solid #ffc107; + color: #856404; + padding: 1rem; + border-radius: 6px; + margin-bottom: 1rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.drive-access-prompt p { + margin: 0; + font-weight: 500; +} + +.drive-access-prompt .sync-btn { + flex-shrink: 0; + white-space: nowrap; + text-decoration: none; + display: inline-block; + padding: 0.5rem 1rem; + text-align: center; +} + .sync-status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); @@ -10228,7 +10298,8 @@ body { } .image-change-btn, -.image-delete-btn { +.image-delete-btn, +.image-add-btn { padding: 0.35rem 0.75rem; border: none; border-radius: 4px; @@ -10255,8 +10326,18 @@ body { background: #c00; } +.image-add-btn { + background: rgba(0,123,255,0.9); + color: #fff; +} + +.image-add-btn:hover { + background: #007bff; +} + .image-change-btn:disabled, -.image-delete-btn:disabled { +.image-delete-btn:disabled, +.image-add-btn:disabled { opacity: 0.5; cursor: not-allowed; } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 128daf63..14482d9a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -297,13 +297,16 @@ function AppContent() { // Fetch moderation pending count for admin badge const refreshModerationCount = useCallback(async () => { + console.log('[App] Refreshing moderation count...'); try { const response = await fetch('/api/admin/moderation/queue/count', { credentials: 'include' }); if (response.ok) { const data = await response.json(); + console.log('[App] New moderation count:', data.count); setModerationCount(data.count); } } catch (err) { + console.error('[App] Failed to refresh moderation count:', err); // Silently ignore — badge just won't show } }, []); @@ -312,7 +315,18 @@ function AppContent() { if (!isAdmin) return; refreshModerationCount(); const interval = setInterval(refreshModerationCount, 60000); - return () => clearInterval(interval); + + // Listen for media upload events to refresh count + const handleCountChanged = () => { + console.log('[App] Received moderation-count-changed event'); + refreshModerationCount(); + }; + window.addEventListener('moderation-count-changed', handleCountChanged); + + return () => { + clearInterval(interval); + window.removeEventListener('moderation-count-changed', handleCountChanged); + }; }, [isAdmin, refreshModerationCount]); // Preview coordinates for real-time editing sync between Map and Sidebar @@ -1879,6 +1893,7 @@ function AppContent() { navigate('/mtb-trail-status'); }} isAdmin={isAdmin} + user={user} editMode={editMode} onDestinationUpdate={handleDestinationUpdate} onDestinationDelete={handleDestinationDelete} diff --git a/frontend/src/components/ImageUploader.jsx b/frontend/src/components/ImageUploader.jsx index 6364aa51..4309ac56 100644 --- a/frontend/src/components/ImageUploader.jsx +++ b/frontend/src/components/ImageUploader.jsx @@ -1,4 +1,5 @@ import React, { useState, useRef } from 'react'; +import MediaUploadModal from './MediaUploadModal'; function ImageUploader({ destinationId, @@ -7,10 +8,14 @@ function ImageUploader({ onPendingImageChange, disabled, isVirtualPoi, - updatedAt + updatedAt, + user, + poiId, + onMediaUpdate }) { const [error, setError] = useState(null); const [dragActive, setDragActive] = useState(false); + const [uploadModalOpen, setUploadModalOpen] = useState(false); const fileInputRef = useRef(null); // Determine what to show: @@ -138,6 +143,16 @@ function ImageUploader({ fileInputRef.current?.click(); }; + const handleUploadSuccess = () => { + setUploadModalOpen(false); + if (onMediaUpdate) { + onMediaUpdate(); + } + }; + + // Check if user is media_admin or admin + const isMediaAdmin = user && (user.role === 'media_admin' || user.role === 'admin'); + return (
@@ -177,6 +192,19 @@ function ImageUploader({ > Delete + {isMediaAdmin && poiId && ( + + )}
) : ( @@ -211,6 +239,15 @@ function ImageUploader({ style={{ display: 'none' }} disabled={disabled} /> + + {/* Upload Modal for additional images (admin only) */} + {uploadModalOpen && poiId && ( + setUploadModalOpen(false)} + onSuccess={handleUploadSuccess} + /> + )} ); } diff --git a/frontend/src/components/Lightbox.css b/frontend/src/components/Lightbox.css index 2f1003a9..72fbc7eb 100644 --- a/frontend/src/components/Lightbox.css +++ b/frontend/src/components/Lightbox.css @@ -6,7 +6,7 @@ right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.95); - z-index: 9999; + z-index: 10001; display: flex; align-items: center; justify-content: center; @@ -120,9 +120,9 @@ /* YouTube Container */ .lightbox-youtube-container { position: relative; - width: 100%; - max-width: 1280px; - aspect-ratio: 16 / 9; + width: min(90vw, 1280px); + height: min(calc(90vw * 9 / 16), calc((100vh - 280px) * 0.9)); + max-height: 720px; } .lightbox-youtube { @@ -132,6 +132,7 @@ width: 100%; height: 100%; border-radius: 4px; + border: none; } /* Caption */ @@ -157,6 +158,41 @@ z-index: 10001; } +/* Pending Review Badge */ +.lightbox-pending-badge { + position: absolute; + top: 70px; + left: 50%; + transform: translateX(-50%); + background: rgba(255, 165, 0, 0.95); + color: white; + padding: 10px 20px; + border-radius: 20px; + font-size: 15px; + font-weight: 600; + z-index: 10001; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Primary Image Badge */ +.lightbox-primary-badge { + position: absolute; + top: 70px; + left: 50%; + transform: translateX(-50%); + background: rgba(255, 193, 7, 0.95); + color: #333; + padding: 12px 24px; + border-radius: 25px; + font-size: 18px; + font-weight: 700; + z-index: 10001; + text-transform: uppercase; + letter-spacing: 1px; + box-shadow: 0 4px 12px rgba(255, 193, 7, 0.4); +} + /* Thumbnail Strip */ .lightbox-thumbnails { position: absolute; @@ -220,7 +256,8 @@ /* Thumbnail Media Indicators */ .thumbnail-video-indicator, -.thumbnail-youtube-indicator { +.thumbnail-youtube-indicator, +.thumbnail-pending-indicator { position: absolute; bottom: 4px; right: 4px; @@ -232,6 +269,124 @@ font-weight: bold; } +.thumbnail-pending-indicator { + background: rgba(255, 165, 0, 0.9); + bottom: 4px; + left: 4px; + right: auto; +} + +/* Set as Primary Button */ +.lightbox-set-primary { + position: absolute; + bottom: 90px; + right: 20px; + width: 180px; + height: 48px; + background: #28a745; + color: white; + border: none; + padding: 0 24px; + border-radius: 6px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + z-index: 10002; + transition: background-color 0.2s, transform 0.1s; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: center; +} + +.lightbox-set-primary:hover { + background: #218838; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.lightbox-set-primary:active { + transform: translateY(0); +} + +.lightbox-set-primary:disabled { + background: #6c757d; + cursor: not-allowed; + opacity: 0.6; +} + +/* Delete Media Button */ +.lightbox-delete-media { + position: absolute; + bottom: 30px; + left: 20px; + width: 180px; + height: 48px; + background: #dc3545; + color: white; + border: none; + padding: 0 24px; + border-radius: 6px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + z-index: 10002; + transition: background-color 0.2s, transform 0.1s; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: center; +} + +.lightbox-delete-media:hover { + background: #c82333; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.lightbox-delete-media:active { + transform: translateY(0); +} + +.lightbox-delete-media:disabled { + background: #6c757d; + cursor: not-allowed; + opacity: 0.6; +} + +/* Add Media Button */ +.lightbox-add-media { + position: absolute; + bottom: 30px; + right: 20px; + width: 180px; + height: 48px; + background: #28a745; + color: white; + border: none; + padding: 0 24px; + border-radius: 6px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + z-index: 10002; + transition: background-color 0.2s, transform 0.1s; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: center; +} + +.lightbox-add-media:hover { + background: #218838; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.lightbox-add-media:active { + transform: translateY(0); +} + /* Mobile Responsiveness */ @media (max-width: 768px) { .lightbox-container { @@ -268,9 +423,37 @@ bottom: 10px; } - .lightbox-thumbnail { - width: 60px; - height: 45px; + .lightbox-youtube-container { + width: calc(100vw - 40px); + height: calc((100vw - 40px) * 9 / 16); + max-height: none; + } + + .lightbox-set-primary { + bottom: 85px; + right: 10px; + width: 160px; + height: 44px; + padding: 0 16px; + font-size: 13px; + } + + .lightbox-delete-media { + bottom: 30px; + left: 10px; + width: 160px; + height: 44px; + padding: 0 16px; + font-size: 13px; + } + + .lightbox-add-media { + bottom: 30px; + right: 10px; + width: 160px; + height: 44px; + padding: 0 16px; + font-size: 13px; } } diff --git a/frontend/src/components/Lightbox.jsx b/frontend/src/components/Lightbox.jsx index b5ae9478..3ad3f37b 100644 --- a/frontend/src/components/Lightbox.jsx +++ b/frontend/src/components/Lightbox.jsx @@ -1,13 +1,19 @@ import { useState, useEffect, useCallback } from 'react'; +import { createPortal } from 'react-dom'; +import MediaUploadModal from './MediaUploadModal'; import './Lightbox.css'; /** * Lightbox Component * Full-screen media viewer with prev/next navigation * Supports images, videos, and YouTube embeds + * Uses React Portal to render outside sidebar DOM */ -function Lightbox({ media, initialIndex = 0, onClose, poiId }) { +function Lightbox({ media, initialIndex = 0, onClose, poiId, user, onMediaUpdate }) { const [currentIndex, setCurrentIndex] = useState(initialIndex); + const [uploadModalOpen, setUploadModalOpen] = useState(false); + const [deleting, setDeleting] = useState(false); + const [settingPrimary, setSettingPrimary] = useState(false); const handlePrevious = useCallback(() => { setCurrentIndex((prev) => (prev > 0 ? prev - 1 : media.length - 1)); @@ -45,6 +51,88 @@ function Lightbox({ media, initialIndex = 0, onClose, poiId }) { const currentMedia = media[currentIndex]; + const handleUploadSuccess = () => { + setUploadModalOpen(false); + if (onMediaUpdate) { + onMediaUpdate(); + } + }; + + const handleDelete = async () => { + if (!window.confirm('Are you sure you want to delete this image?')) { + return; + } + + setDeleting(true); + try { + const response = await fetch(`/api/pois/${poiId}/media/${currentMedia.id}`, { + method: 'DELETE', + credentials: 'include' + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to delete'); + } + + if (media.length === 1) { + onClose(); + if (onMediaUpdate) { + onMediaUpdate(); + } + return; + } + + let newIndex = currentIndex; + if (currentIndex >= media.length - 1) { + newIndex = Math.max(0, currentIndex - 1); + } + + setCurrentIndex(newIndex); + + if (onMediaUpdate) { + onMediaUpdate(); + } + } catch (error) { + console.error('Delete failed:', error); + alert('Failed to delete image: ' + error.message); + } finally { + setDeleting(false); + } + }; + + const handleSetPrimary = async () => { + if (!window.confirm('Set this as the primary image? The current primary will become a gallery image.')) { + return; + } + + const currentMediaId = currentMedia.id; + setSettingPrimary(true); + try { + const response = await fetch(`/api/pois/${poiId}/media/${currentMedia.id}/set-primary`, { + method: 'PATCH', + credentials: 'include' + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to set primary'); + } + + if (onMediaUpdate) { + onMediaUpdate(); + } + + window.dispatchEvent(new CustomEvent('poi-updated', { detail: { poiId } })); + setCurrentIndex(0); + } catch (error) { + console.error('Set primary failed:', error); + alert('Failed to set as primary: ' + error.message); + } finally { + setSettingPrimary(false); + } + }; + const renderMedia = () => { if (currentMedia.media_type === 'youtube') { return ( @@ -81,7 +169,7 @@ function Lightbox({ media, initialIndex = 0, onClose, poiId }) { } }; - return ( + return createPortal(
e.stopPropagation()}> {/* Close Button */} @@ -125,6 +213,20 @@ function Lightbox({ media, initialIndex = 0, onClose, poiId }) {
)} + {/* Pending indicator for user's own uploads */} + {currentMedia.moderation_status === 'pending' && ( +
+ ⏱ Pending Review +
+ )} + + {/* Primary image indicator */} + {currentMedia.role === 'primary' && ( +
+ ⭐ Primary Image +
+ )} + {/* Counter */}
{currentIndex + 1} / {media.length} @@ -158,12 +260,71 @@ function Lightbox({ media, initialIndex = 0, onClose, poiId }) { {item.media_type === 'youtube' && (
YT
)} + {item.moderation_status === 'pending' && ( +
+ )}
))}
)} + + {/* Set as Primary button for admins on non-primary published images */} + {user && (user.role === 'admin' || user.role === 'media_admin') && + currentMedia.role !== 'primary' && + ['published', 'auto_approved'].includes(currentMedia.moderation_status) && ( + + )} + + {/* Delete button for user's own uploads or admins */} + {user && (currentMedia.uploaded_by_user || user.role === 'admin' || user.role === 'media_admin') && ( + + )} + + {/* Add Photo/Video button for authenticated users */} + {user && ( + + )} - + + {/* Upload Modal */} + {uploadModalOpen && ( + setUploadModalOpen(false)} + onSuccess={handleUploadSuccess} + /> + )} + , + document.body ); } diff --git a/frontend/src/components/MediaUploadModal.css b/frontend/src/components/MediaUploadModal.css index 5a600134..e024c75f 100644 --- a/frontend/src/components/MediaUploadModal.css +++ b/frontend/src/components/MediaUploadModal.css @@ -257,12 +257,12 @@ } .btn-primary { - background: #007bff; + background: #28a745; color: white; } .btn-primary:hover:not(:disabled) { - background: #0056b3; + background: #218838; } .btn-primary:disabled { diff --git a/frontend/src/components/MediaUploadModal.jsx b/frontend/src/components/MediaUploadModal.jsx index 6fb8ae2a..e5493862 100644 --- a/frontend/src/components/MediaUploadModal.jsx +++ b/frontend/src/components/MediaUploadModal.jsx @@ -23,11 +23,20 @@ function MediaUploadModal({ poiId, onClose, onSuccess }) { if (!file) return; const isVideo = activeTab === 'video'; + const fileName = file.name.toLowerCase(); + const allowedTypes = isVideo ? ['video/mp4', 'video/webm', 'video/quicktime'] : ['image/jpeg', 'image/png', 'image/webp']; - if (!allowedTypes.includes(file.type)) { + const allowedExtensions = isVideo + ? ['.mp4', '.webm', '.mov'] + : ['.jpg', '.jpeg', '.png', '.webp']; + + const hasValidType = allowedTypes.includes(file.type); + const hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext)); + + if (!hasValidType && !hasValidExtension) { const expected = isVideo ? 'MP4, WebM, or MOV' : 'JPEG, PNG, or WebP'; setError(`Please select a ${expected} file`); return; @@ -180,8 +189,8 @@ function MediaUploadModal({ poiId, onClose, onSuccess }) { type="file" accept={ activeTab === 'video' - ? 'video/mp4,video/webm,video/quicktime' - : 'image/jpeg,image/png,image/webp' + ? 'video/mp4,video/webm,video/quicktime,.mp4,.webm,.mov' + : 'image/jpeg,image/png,image/webp,.jpg,.jpeg,.png,.webp' } onChange={(e) => handleFileSelect(e.target.files[0])} style={{ display: 'none' }} diff --git a/frontend/src/components/ModerationInbox.jsx b/frontend/src/components/ModerationInbox.jsx index ec09a724..eef59a9f 100644 --- a/frontend/src/components/ModerationInbox.jsx +++ b/frontend/src/components/ModerationInbox.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; +import Lightbox from './Lightbox'; const FIELD_CONFIGS = { news: [ @@ -28,6 +29,7 @@ const FIELD_CONFIGS = { }; function ModerationInbox({ onCountChange }) { + console.log('[ModerationInbox] Mounted with onCountChange:', !!onCountChange); const [queue, setQueue] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); @@ -53,6 +55,10 @@ function ModerationInbox({ onCountChange }) { const [addingUrl, setAddingUrl] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [searchInput, setSearchInput] = useState(''); + const [lightboxMedia, setLightboxMedia] = useState(null); + const [lightboxIndex, setLightboxIndex] = useState(0); + const [lightboxPoiId, setLightboxPoiId] = useState(null); + const [user, setUser] = useState(null); const LIMIT = 20; const fetchQueue = useCallback(async () => { @@ -90,27 +96,108 @@ function ModerationInbox({ onCountChange }) { .then(r => r.ok ? r.json() : []) .then(data => setPois(Array.isArray(data) ? data : [])) .catch(() => setPois([])); + + fetch('/api/user', { credentials: 'include' }) + .then(r => r.ok ? r.json() : null) + .then(data => setUser(data)) + .catch(() => setUser(null)); }, []); const notify = (type, message) => setNotification({ type, message }); + const getThumbnailUrl = (item) => { + if (!item.media_type) return null; + + if (item.media_type === 'youtube') { + // Extract video ID from YouTube URL + const videoId = item.source_url?.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/)?.[1]; + return videoId ? `https://img.youtube.com/vi/${videoId}/mqdefault.jpg` : null; + } else if (item.image_server_asset_id && (item.media_type === 'image' || item.media_type === 'video')) { + return `/api/assets/${item.image_server_asset_id}/thumbnail`; + } + return null; + }; + + const handleOpenLightbox = async (item) => { + if (!item.poi_id) return; + + try { + // Fetch all media for this POI + const response = await fetch(`/api/pois/${item.poi_id}/media`, { credentials: 'include' }); + if (!response.ok) return; + + const data = await response.json(); + const allMedia = data.all_media || []; + + // Find the index of the clicked item + const index = allMedia.findIndex(m => m.id === item.id); + + setLightboxMedia(allMedia); + setLightboxIndex(index >= 0 ? index : 0); + setLightboxPoiId(item.poi_id); + } catch (err) { + console.error('Failed to load media for lightbox:', err); + } + }; + + const handleLightboxClose = () => { + setLightboxMedia(null); + setLightboxIndex(0); + setLightboxPoiId(null); + }; + + const handleMediaUpdate = () => { + fetchQueue(); + if (lightboxPoiId) { + // Refresh lightbox media + fetch(`/api/pois/${lightboxPoiId}/media`, { credentials: 'include' }) + .then(r => r.json()) + .then(data => setLightboxMedia(data.all_media || [])) + .catch(err => console.error('Failed to refresh lightbox media:', err)); + } + }; + const handleApprove = async (type, id) => { try { + // Find the item to get its POI ID before approval + const item = queue.find(q => q.content_type === type && q.id === id); + console.log('[Moderation] Approving:', { type, id, item }); const response = await fetch('/api/admin/moderation/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ type, id }) }); - if (response.ok) { notify('success', `${type} #${id} approved`); fetchQueue(); if (onCountChange) onCountChange(); } + if (response.ok) { + notify('success', `${type} #${id} approved`); + fetchQueue(); + console.log('[Moderation] Calling onCountChange:', !!onCountChange); + if (onCountChange) onCountChange(); + // Emit event to refresh media for this POI + if (type === 'photo' && item?.poi_id) { + console.log('[Moderation] Emitting poi-media-updated event for POI', item.poi_id); + window.dispatchEvent(new CustomEvent('poi-media-updated', { detail: { poiId: item.poi_id } })); + // Also emit event to refresh map markers (in case this was a primary image change) + window.dispatchEvent(new CustomEvent('poi-updated', { detail: { poiId: item.poi_id } })); + } + } } catch (err) { notify('error', err.message); } }; const handleReject = async (type, id) => { try { + const item = queue.find(q => q.content_type === type && q.id === id); const response = await fetch('/api/admin/moderation/reject', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ type, id, reason: '' }) }); - if (response.ok) { notify('success', `${type} #${id} rejected`); fetchQueue(); if (onCountChange) onCountChange(); } + if (response.ok) { + notify('success', `${type} #${id} rejected`); + fetchQueue(); + if (onCountChange) onCountChange(); + // Emit event to refresh media for this POI + if (type === 'photo' && item?.poi_id) { + window.dispatchEvent(new CustomEvent('poi-media-updated', { detail: { poiId: item.poi_id } })); + } + } } catch (err) { notify('error', err.message); } }; @@ -120,6 +207,14 @@ function ModerationInbox({ onCountChange }) { const [type, id] = key.split(':'); return { type, id: parseInt(id) }; }); + // Collect unique POI IDs for photo items + const photoPoiIds = new Set(); + items.forEach(({ type, id }) => { + if (type === 'photo') { + const item = queue.find(q => q.content_type === type && q.id === id); + if (item?.poi_id) photoPoiIds.add(item.poi_id); + } + }); try { const response = await fetch('/api/admin/moderation/bulk-approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -131,6 +226,10 @@ function ModerationInbox({ onCountChange }) { setSelectedItems(new Set()); fetchQueue(); if (onCountChange) onCountChange(); + // Emit events for all affected POIs + photoPoiIds.forEach(poiId => { + window.dispatchEvent(new CustomEvent('poi-media-updated', { detail: { poiId } })); + }); } } catch (err) { notify('error', err.message); } }; @@ -331,21 +430,36 @@ function ModerationInbox({ onCountChange }) { }; const handleSave = async (type, id) => { + console.log('[Moderation] Saving:', { type, id, edits: editFields }); + const item = queue.find(q => q.content_type === type && q.id === id); try { const response = await fetch('/api/admin/moderation/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ type, id, edits: editFields }) }); if (response.ok) { + console.log('[Moderation] Save successful'); notify('success', `${type} #${id} saved`); setEditingItem(null); setEditFields({}); fetchQueue(); + // Emit event to refresh media for this POI (in case caption or POI changed) + if (type === 'photo' && item?.poi_id) { + window.dispatchEvent(new CustomEvent('poi-media-updated', { detail: { poiId: item.poi_id } })); + // Also emit for the new POI if it changed + if (editFields.poi_id && editFields.poi_id !== item.poi_id) { + window.dispatchEvent(new CustomEvent('poi-media-updated', { detail: { poiId: editFields.poi_id } })); + } + } } else { const err = await response.json(); + console.error('[Moderation] Save failed:', err); notify('error', err.error || 'Save failed'); } - } catch (err) { notify('error', err.message); } + } catch (err) { + console.error('[Moderation] Save error:', err); + notify('error', err.message); + } }; const handleCreate = async () => { @@ -762,6 +876,64 @@ function ModerationInbox({ onCountChange }) { )} + {/* Thumbnail for media items */} + {item.content_type === 'photo' && getThumbnailUrl(item) && ( +
handleOpenLightbox(item)} + style={{ + width: '120px', + height: '90px', + borderRadius: '6px', + overflow: 'hidden', + cursor: 'pointer', + margin: '6px 0', + border: '1px solid #e0e0e0', + position: 'relative', + flexShrink: 0 + }} + > + {item.title + {item.media_type === 'video' && ( +
+ ▶ +
+ )} + {item.media_type === 'youtube' && ( +
+ YT +
+ )} +
+ )} + {/* Event dates */} {item.content_type === 'event' && (item.start_date || item.end_date) && (
@@ -776,16 +948,18 @@ function ModerationInbox({ onCountChange }) {

{item.description}

)} - setExpandedItem(isExpanded ? null : itemKey)} - style={{ color: '#4a7c23', fontSize: '0.8rem', cursor: 'pointer', textDecoration: 'none', fontWeight: 500 }}> - {isExpanded ? 'Show less' : 'Show more'} - + {item.description && item.description.length > 200 && ( + setExpandedItem(isExpanded ? null : itemKey)} + style={{ color: '#4a7c23', fontSize: '0.8rem', cursor: 'pointer', textDecoration: 'none', fontWeight: 500 }}> + {isExpanded ? 'Show less' : 'Show more'} + + )} {/* Source URL (expanded, read-only) */} {isExpanded && item.source_url && ( @@ -1009,6 +1183,18 @@ function ModerationInbox({ onCountChange }) { {notification.message}
)} + + {/* Lightbox */} + {lightboxMedia && ( + + )} ); } diff --git a/frontend/src/components/Mosaic.css b/frontend/src/components/Mosaic.css index a77d5ddb..b466cf5d 100644 --- a/frontend/src/components/Mosaic.css +++ b/frontend/src/components/Mosaic.css @@ -1,12 +1,13 @@ /* Mosaic Container */ .mosaic { width: 100%; + height: 200px; display: grid; gap: 4px; - border-radius: 8px; + border-radius: 0; overflow: hidden; cursor: pointer; - margin-bottom: 16px; + flex-shrink: 0; } /* Single image - full width */ @@ -81,6 +82,34 @@ background-color: rgba(0, 0, 0, 0.8); } +/* Primary Image Indicator */ +.mosaic-primary-indicator { + position: absolute; + top: 8px; + right: 8px; + font-size: 20px; + pointer-events: none; + filter: grayscale(100%) brightness(1.2); + opacity: 0.7; + text-shadow: 0 0 4px rgba(0, 0, 0, 0.8); +} + +/* Pending Review Indicator */ +.mosaic-pending-indicator { + position: absolute; + top: 8px; + left: 8px; + background-color: rgba(255, 165, 0, 0.9); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + pointer-events: none; + text-transform: uppercase; + letter-spacing: 0.5px; +} + /* More Photos Overlay */ .mosaic-more-overlay { position: absolute; diff --git a/frontend/src/components/Mosaic.jsx b/frontend/src/components/Mosaic.jsx index 70dbd695..cba972f4 100644 --- a/frontend/src/components/Mosaic.jsx +++ b/frontend/src/components/Mosaic.jsx @@ -7,7 +7,7 @@ import './Mosaic.css'; * Displays 1-3 images in a Facebook-style mosaic layout * Click opens lightbox with all media */ -function Mosaic({ media, poiId }) { +function Mosaic({ media, poiId, user, onMediaUpdate }) { const [lightboxOpen, setLightboxOpen] = useState(false); const [lightboxIndex, setLightboxIndex] = useState(0); @@ -57,10 +57,20 @@ function Mosaic({ media, poiId }) { {item.media_type === 'youtube' && (
- +
)} + {/* Primary indicator */} + {item.role === 'primary' && ( +
+ )} + {/* Pending indicator for user's own uploads */} + {item.moderation_status === 'pending' && ( +
+ Pending Review +
+ )} {/* Show count overlay on last image if there are more */} {index === 2 && media.length > 3 && (
@@ -77,6 +87,8 @@ function Mosaic({ media, poiId }) { initialIndex={lightboxIndex} onClose={handleCloseLightbox} poiId={poiId} + user={user} + onMediaUpdate={onMediaUpdate} /> )} diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index e09d7f64..b7a9205f 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -209,121 +209,10 @@ function EditableCellSignal({ level, onChange }) { } // Read-only view component - works for both destinations and linear features -function ReadOnlyView({ destination, isLinearFeature, isAdmin, editMode, showImage = true, onShare, moreInfoLink, trailStatus = null, _showNpsMap, _onToggleNpsMap, onCollectStatus }) { - // Multi-media state - const [media, setMedia] = useState([]); - const [mediaLoading, setMediaLoading] = useState(false); - const [uploadModalOpen, setUploadModalOpen] = useState(false); - const [user, setUser] = useState(null); - - // Check authentication status - useEffect(() => { - fetch('/api/auth/status', { credentials: 'include' }) - .then(res => res.ok ? res.json() : null) - .then(data => setUser(data?.user || null)) - .catch(() => setUser(null)); - }, []); - - // Fetch media for this POI - useEffect(() => { - if (!destination?.id) return; - - setMediaLoading(true); - fetch(`/api/pois/${destination.id}/media`, { credentials: 'include' }) - .then(res => res.ok ? res.json() : { mosaic: [], all_media: [] }) - .then(data => { - setMedia(data.all_media || []); - }) - .catch(err => { - console.error('Failed to load media:', err); - setMedia([]); - }) - .finally(() => setMediaLoading(false)); - }, [destination?.id]); - - // Legacy: Use thumbnail service for backward compatibility - // Include updated_at for cache busting when image changes - const imageUrl = destination.has_primary_image - ? `/api/pois/${destination.id}/thumbnail?size=medium&v=${destination.updated_at || Date.now()}` - : null; - - // Get default thumbnail SVG path based on type - const getDefaultThumbnail = () => { - if (isLinearFeature) { - if (destination.feature_type === 'river') return '/icons/thumbnails/river.svg'; - if (destination.feature_type === 'boundary') return '/icons/thumbnails/boundary.svg'; - return '/icons/thumbnails/trail.svg'; - } - if (destination.poi_type === 'virtual') return '/icons/thumbnails/virtual.svg'; - // MTB trailheads are point POIs with status_url - if (destination.poi_type === 'point' && destination.status_url) return '/icons/thumbnails/trail.svg'; - return '/icons/thumbnails/destination.svg'; - }; - - const handleUploadSuccess = () => { - // Refresh media after successful upload - fetch(`/api/pois/${destination.id}/media`, { credentials: 'include' }) - .then(res => res.json()) - .then(data => setMedia(data.all_media || [])) - .catch(err => console.error('Failed to refresh media:', err)); - }; - +function ReadOnlyView({ destination, isLinearFeature, isAdmin, editMode, onShare, moreInfoLink, trailStatus = null, _showNpsMap, _onToggleNpsMap, onCollectStatus }) { return (
- {/* Multi-media section */} - {showImage && ( -
- {media.length > 0 ? ( - - ) : !mediaLoading && destination.has_primary_image ? ( - // Fallback: Show single image if media endpoint returned empty but has_primary_image is true -
- {destination.name} -
- ) : !mediaLoading ? ( - // No images - show default thumbnail -
- {destination.name} -
- ) : ( - // Loading state -
-

Loading media...

-
- )} - - {/* Add Media button for authenticated users */} - {user && ( - - )} -
- )} - - {/* Upload Modal */} - {uploadModalOpen && ( - setUploadModalOpen(false)} - onSuccess={handleUploadSuccess} - /> - )}
@@ -482,7 +371,7 @@ function ReadOnlyView({ destination, isLinearFeature, isAdmin, editMode, showIma } // Edit view component - works for both destinations and linear features -function EditView({ destination, editedData, setEditedData, onSave, onCancel, onDelete, saving, deleting, onPreviewCoordsChange, isNewPOI, isNewOrganization, _onImageUpdate, isLinearFeature, showImage = true }) { +function EditView({ destination, editedData, setEditedData, onSave, onCancel, onDelete, saving, deleting, onPreviewCoordsChange, isNewPOI, isNewOrganization, _onImageUpdate, isLinearFeature }) { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [aiError, setAiError] = useState(null); // Prompt editor modal state @@ -508,6 +397,23 @@ function EditView({ destination, editedData, setEditedData, onSave, onCancel, on // Pending image state (staging area for image uploads until save) const [pendingImage, setPendingImage] = useState(null); + // User state for admin checks + const [user, setUser] = useState(null); + + // Check authentication status + useEffect(() => { + fetch('/api/auth/status', { credentials: 'include' }) + .then(res => res.ok ? res.json() : null) + .then(data => setUser(data?.user || null)) + .catch(() => setUser(null)); + }, []); + + // Callback for media updates from ImageUploader (no-op, just for consistency) + const handleMediaUpdate = () => { + // In edit mode, media updates will be handled by the parent refresh + // This is just a placeholder for the ImageUploader interface + }; + // Handle save with pending image processing const handleSaveWithImage = async () => { if (!destination?.id) { @@ -874,6 +780,9 @@ function EditView({ destination, editedData, setEditedData, onSave, onCancel, on updatedAt={editedData.updated_at} disabled={saving} isVirtualPoi={destination?.poi_type === 'virtual'} + user={user} + poiId={destination.id} + onMediaUpdate={handleMediaUpdate} /> ) : (
@@ -2785,7 +2694,7 @@ function TrailStatus({ poiId, _poiName, isAdmin, editMode, _selectedFromMtbList, ); } -function Sidebar({ destination, isNewPOI, newOrganization, isNewOrganization, onClose, isAdmin, editMode, onDestinationUpdate, onDestinationDelete, onSaveNewPOI, onCancelNewPOI, onSaveNewOrganization, onCancelNewOrganization, previewCoords, onPreviewCoordsChange, linearFeature, onLinearFeatureUpdate, onLinearFeatureDelete, onNavigate, currentIndex, totalCount, poiNavigationList, associations, allDestinations, allLinearFeatures, allVirtualPois, onSelectDestination, onSelectLinearFeature, onAssociationsChanged, onStartDrawingAssociations, isInMtbMode, selectedFromMtbList, mtbTrailsList, currentMtbIndex, onNavigateMtbTrail, onBackToMtbList, showNpsMap, onToggleNpsMap }) { +function Sidebar({ destination, isNewPOI, newOrganization, isNewOrganization, onClose, isAdmin, user, editMode, onDestinationUpdate, onDestinationDelete, onSaveNewPOI, onCancelNewPOI, onSaveNewOrganization, onCancelNewOrganization, previewCoords, onPreviewCoordsChange, linearFeature, onLinearFeatureUpdate, onLinearFeatureDelete, onNavigate, currentIndex, totalCount, poiNavigationList, associations, allDestinations, allLinearFeatures, allVirtualPois, onSelectDestination, onSelectLinearFeature, onAssociationsChanged, onStartDrawingAssociations, isInMtbMode, selectedFromMtbList, mtbTrailsList, currentMtbIndex, onNavigateMtbTrail, onBackToMtbList, showNpsMap, onToggleNpsMap }) { const [isEditing, setIsEditing] = useState(false); const [editedData, setEditedData] = useState({}); const [saving, setSaving] = useState(false); @@ -2795,6 +2704,7 @@ function Sidebar({ destination, isNewPOI, newOrganization, isNewOrganization, on const [showAssociationsModal, setShowAssociationsModal] = useState(false); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const [pendingImage, setPendingImage] = useState(null); + const [uploadModalOpen, setUploadModalOpen] = useState(false); const [, setNewsCount] = useState(0); const [, setEventsCount] = useState(0); const [, setCollectResult] = useState(null); @@ -2815,6 +2725,83 @@ function Sidebar({ destination, isNewPOI, newOrganization, isNewOrganization, on } }, [newOrganization]); + // Media state for top-level image/mosaic display + const [media, setMedia] = useState([]); + const [mediaLoading, setMediaLoading] = useState(false); + + // Fetch media for destination/linear feature + useEffect(() => { + const poiId = destination?.id || linearFeature?.id; + if (!poiId) return; + + setMediaLoading(true); + fetch(`/api/pois/${poiId}/media`, { credentials: 'include' }) + .then(res => res.ok ? res.json() : { all_media: [] }) + .then(data => { + setMedia(data.all_media || []); + setMediaLoading(false); + }) + .catch(err => { + console.error('Failed to load media:', err); + setMedia([]); + setMediaLoading(false); + }); + }, [destination?.id, linearFeature?.id]); + + // Listen for media updates from moderation queue + useEffect(() => { + const poiId = destination?.id || linearFeature?.id; + if (!poiId) return; + + const handleMediaUpdateEvent = (event) => { + if (event.detail.poiId === poiId) { + console.log('[Sidebar] POI media updated for', poiId, '- refreshing...'); + // Refresh media list + fetch(`/api/pois/${poiId}/media`, { credentials: 'include' }) + .then(res => res.json()) + .then(data => setMedia(data.all_media || [])) + .catch(err => console.error('[Sidebar] Failed to refresh media:', err)); + + // Refresh POI data to update has_primary_image flag + fetch(`/api/pois/${poiId}`, { credentials: 'include' }) + .then(res => res.json()) + .then(data => { + if (destination && onDestinationUpdate) { + onDestinationUpdate(data); + } + }) + .catch(err => console.error('[Sidebar] Failed to refresh POI:', err)); + } + }; + + window.addEventListener('poi-media-updated', handleMediaUpdateEvent); + return () => window.removeEventListener('poi-media-updated', handleMediaUpdateEvent); + }, [destination?.id, linearFeature?.id]); + + // Callback for media updates from ImageUploader or Mosaic + const handleMediaUpdate = () => { + // Refresh media after upload + const poiId = destination?.id || linearFeature?.id; + if (!poiId) return; + + // Refresh media list + fetch(`/api/pois/${poiId}/media`, { credentials: 'include' }) + .then(res => res.json()) + .then(data => setMedia(data.all_media || [])) + .catch(err => console.error('Failed to refresh media:', err)); + + // Refresh POI data to update has_primary_image flag + if (destination && onDestinationUpdate) { + fetch(`/api/pois/${poiId}`, { credentials: 'include' }) + .then(res => res.json()) + .then(data => onDestinationUpdate(data)) + .catch(err => console.error('Failed to refresh POI:', err)); + } + + // Emit event to refresh moderation count (photo uploads go to pending) + window.dispatchEvent(new CustomEvent('moderation-count-changed')); + }; + // Determine which POI we're showing const activePoi = destination || linearFeature; @@ -3396,67 +3383,81 @@ function Sidebar({ destination, isNewPOI, newOrganization, isNewOrganization, on
- {/* Image - always shown at top for all tabs */} - {isEditing && linearFeature?.id ? ( - - ) : ( -
- {linearImageUrl ? ( - {linearFeature?.name} - ) : ( - {linearFeature?.name} - )} - {/* Navigation chevrons on image - mobile only */} - {isMobile && onNavigate && poiNavigationList && poiNavigationList.length > 1 && ( - <> - {currentIndex > 0 && ( - - )} - {currentIndex < poiNavigationList.length - 1 && ( - - )} - - )} -
- )} + {/* Media section - Mosaic in view mode, ImageUploader in edit mode */} +
+ {isEditing && linearFeature?.id ? ( + + ) : media.length > 0 ? ( + + ) : !mediaLoading && linearFeature?.has_primary_image ? ( +
+ {linearFeature?.name} +
+ ) : user && linearFeature?.id && !mediaLoading ? ( +
+ +
+ ) : null} + + {/* Navigation chevrons on image - mobile only */} + {isMobile && !isEditing && onNavigate && poiNavigationList && poiNavigationList.length > 1 && ( + <> + {currentIndex > 0 && ( + + )} + {currentIndex < poiNavigationList.length - 1 && ( + + )} + + )} +
{/* Sidebar Tabs - same as destinations */}
@@ -3507,7 +3508,6 @@ function Sidebar({ destination, isNewPOI, newOrganization, isNewOrganization, on deleting={deleting} isNewPOI={false} isLinearFeature={true} - showImage={false} onImageUpdate={(hasImage, driveFileId) => { if (onLinearFeatureUpdate) { onLinearFeatureUpdate({ @@ -3523,7 +3523,6 @@ function Sidebar({ destination, isNewPOI, newOrganization, isNewOrganization, on isLinearFeature={true} isAdmin={isAdmin} editMode={editMode} - showImage={false} onShare={() => setShowShareModal(true)} moreInfoLink={linearFeature.more_info_link} trailStatus={trailStatus} @@ -3718,67 +3717,82 @@ function Sidebar({ destination, isNewPOI, newOrganization, isNewOrganization, on
- {/* Image - always shown at top for all tabs */} - {isEditing && destination?.id ? ( - - ) : ( -
- {imageUrl ? ( - {destination?.name} - ) : ( - {destination?.name} - )} - {/* Navigation chevrons on image - mobile only */} - {isMobile && onNavigate && poiNavigationList && poiNavigationList.length > 1 && ( - <> - {currentIndex > 0 && ( - - )} - {currentIndex < poiNavigationList.length - 1 && ( - - )} - - )} -
- )} + {/* Media section - Mosaic in view mode, ImageUploader in edit mode */} +
+ {isEditing && destination?.id ? ( + + ) : media.length > 0 ? ( + + ) : !mediaLoading && destination?.has_primary_image ? ( +
+ {destination?.name} +
+ ) : user && destination?.id && !mediaLoading ? ( +
+ +
+ ) : null} + + {/* Navigation chevrons on image - mobile only */} + {isMobile && !isEditing && onNavigate && poiNavigationList && poiNavigationList.length > 1 && ( + <> + {currentIndex > 0 && ( + + )} + {currentIndex < poiNavigationList.length - 1 && ( + + )} + + )} +
{/* Sidebar Tabs - always shown */}
@@ -3999,6 +4013,17 @@ function Sidebar({ destination, isNewPOI, newOrganization, isNewOrganization, on editMode={editMode} onAssociationsChanged={onAssociationsChanged} /> + + {uploadModalOpen && destination?.id && ( + setUploadModalOpen(false)} + onSuccess={() => { + setUploadModalOpen(false); + handleMediaUpdate(); + }} + /> + )}
); } diff --git a/frontend/src/components/SyncSettings.jsx b/frontend/src/components/SyncSettings.jsx index bc80e97b..efcfd93a 100644 --- a/frontend/src/components/SyncSettings.jsx +++ b/frontend/src/components/SyncSettings.jsx @@ -323,6 +323,14 @@ function SyncSettings({ onDataRefresh, onNavigateToJobs }) { {error &&
{error}
} {message &&
{message}
} + {/* Drive access prompt - shown when admin lacks Drive credentials */} + {syncStatus && !syncStatus.drive_access_verified && ( +
+

⚠️ Drive access required for backup/restore operations.

+ Grant Drive Access +
+ )} +

Backup & Restore