From e1e1f3ab84070e40451f185c99308422f64e180e Mon Sep 17 00:00:00 2001
From: sethwebster <sethwebster@gmail.com>
Date: Fri, 24 Oct 2025 18:03:14 -0400
Subject: [PATCH]   1. API Updates (/api/admin/migrate-redis/route.ts)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

  - Added logs: string[] to the MigrationProgress interface
  - Created an addLog() helper function that:
    - Timestamps each log entry
    - Adds appropriate icons (ℹ️, ⚠️, ❌)
    - Stores logs in the progress object
    - Keeps only the last 100 logs to prevent memory issues
    - Also logs to console for server-side debugging

  2. Enhanced Logging Throughout Migration

  - Connection: Logs when connecting to old and new Redis
  - Scanning: Logs when scanning for keys
  - Migration Start: Logs when migration begins
  - Progress: Logs every 10th key (to avoid spam) with progress counter
  - Skips: Logs when keys already exist and are skipped
  - Errors: Logs any failures with error details
  - Completion: Logs final summary with stats

  3. UI Updates (/admin/migrate-redis/page.tsx)

  - Added new "Migration Logs" section that displays:
    - Live log feed with auto-updating entries
    - Timestamp for each log entry
    - Color-coded messages (red for errors, yellow for warnings, gray for info)
    - Log count in the header
    - Auto-scroll checkbox to follow logs in real-time
    - Max height with scrollable container
  - Separated "Error Summary" section for quick error review
  - Auto-scroll feature that automatically scrolls to the latest log entry

  Features

  ✅ Real-time Updates: Logs refresh every second✅ Auto-scroll: Automatically follows new logs
  (can be disabled)✅ Color-coded: Errors in red, warnings in yellow, info in gray✅ Timestamped:
  Each log entry has a timestamp✅ Memory Efficient: Keeps only last 100 logs✅ Smart Logging: Logs
   every 10th key to avoid spam✅ Dual Output: Logs to both UI and console

  Now you can see exactly what's happening during the migration without having to check the server
  console!
---
 src/app/admin/data/page.tsx              |  35 ++
 src/app/admin/migrate-redis/page.tsx     | 397 +++++++++++++++++++++++
 src/app/admin/page.tsx                   |   6 +
 src/app/api/admin/migrate-redis/route.ts | 350 ++++++++++++++++++++
 4 files changed, 788 insertions(+)
 create mode 100644 src/app/admin/migrate-redis/page.tsx
 create mode 100644 src/app/api/admin/migrate-redis/route.ts

diff --git a/src/app/admin/data/page.tsx b/src/app/admin/data/page.tsx
index 5dc23aa..6227fd2 100644
--- a/src/app/admin/data/page.tsx
+++ b/src/app/admin/data/page.tsx
@@ -6,6 +6,39 @@
 export default function AdminDataPage() {
   return (
     <div className="space-y-6 p-6 md:p-8">
+      {/* Quick Actions */}
+      <div className="bg-primary/10 border border-primary/20 rounded-xl p-4 md:p-6">
+        <h2 className="text-xl md:text-2xl font-bold text-foreground mb-4">
+          Admin Actions
+        </h2>
+        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+          <a
+            href="/admin/migrate-redis"
+            className="flex items-center gap-3 p-4 bg-card border border-border rounded-lg hover:bg-muted transition"
+          >
+            <span className="text-2xl">🔄</span>
+            <div>
+              <h3 className="font-semibold text-foreground">Migrate Redis</h3>
+              <p className="text-xs text-muted-foreground">
+                Move data from old to new Redis instance
+              </p>
+            </div>
+          </a>
+          <a
+            href="/admin/reset"
+            className="flex items-center gap-3 p-4 bg-card border border-destructive/30 rounded-lg hover:bg-destructive/5 transition"
+          >
+            <span className="text-2xl">⚠️</span>
+            <div>
+              <h3 className="font-semibold text-destructive">Reset Communities</h3>
+              <p className="text-xs text-muted-foreground">
+                Clear and re-seed Redis data
+              </p>
+            </div>
+          </a>
+        </div>
+      </div>
+
       {/* System Info */}
       <div className="bg-card border border-border rounded-xl p-4 md:p-6">
         <h2 className="text-xl md:text-2xl font-bold text-foreground mb-4">
@@ -95,6 +128,8 @@ export default function AdminDataPage() {
           <APIEndpoint method="GET" path="/api/communities" description="Fetch all communities (with filters)" />
           <APIEndpoint method="GET" path="/api/communities/stats" description="Get aggregate statistics" />
           <APIEndpoint method="GET" path="/api/communities/[slug]" description="Get single community" />
+          <APIEndpoint method="POST" path="/api/admin/migrate-redis" description="Migrate data from old Redis" />
+          <APIEndpoint method="GET" path="/api/admin/migrate-redis" description="Get migration progress" />
           <APIEndpoint method="POST" path="/api/admin/reset-communities" description="Clear and re-seed Redis" danger />
           <APIEndpoint method="POST" path="/api/admin/merge-communities" description="Merge all sources and seed" danger />
         </div>
diff --git a/src/app/admin/migrate-redis/page.tsx b/src/app/admin/migrate-redis/page.tsx
new file mode 100644
index 0000000..17e4f75
--- /dev/null
+++ b/src/app/admin/migrate-redis/page.tsx
@@ -0,0 +1,397 @@
+/**
+ * Admin Redis Migration Page
+ * Migrate data from old Redis instance to new one
+ */
+
+'use client';
+
+import { useState, useEffect } from 'react';
+
+interface MigrationProgress {
+  status: 'running' | 'completed' | 'failed';
+  progress: number;
+  total: number;
+  currentKey?: string;
+  migratedKeys: number;
+  skippedKeys: number;
+  failedKeys: number;
+  errors: string[];
+  logs: string[];
+  startedAt: string;
+  completedAt?: string;
+}
+
+export default function MigrateRedisPage() {
+  const [oldRedisUrl, setOldRedisUrl] = useState('');
+  const [dryRun, setDryRun] = useState(true);
+  const [migrating, setMigrating] = useState(false);
+  const [migrationId, setMigrationId] = useState<string | null>(null);
+  const [progress, setProgress] = useState<MigrationProgress | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [autoScroll, setAutoScroll] = useState(true);
+
+  // Poll for progress updates
+  useEffect(() => {
+    if (!migrationId || !migrating) return;
+
+    const interval = setInterval(async () => {
+      try {
+        const response = await fetch(
+          `/api/admin/migrate-redis?migrationId=${migrationId}`
+        );
+        const data = await response.json();
+
+        if (response.ok) {
+          setProgress(data);
+
+          // Stop polling if completed or failed
+          if (data.status === 'completed' || data.status === 'failed') {
+            setMigrating(false);
+          }
+        } else {
+          setError(data.error || 'Failed to get migration status');
+          setMigrating(false);
+        }
+      } catch (err) {
+        setError(err instanceof Error ? err.message : 'Unknown error');
+        setMigrating(false);
+      }
+    }, 1000);
+
+    return () => clearInterval(interval);
+  }, [migrationId, migrating]);
+
+  // Auto-scroll logs to bottom
+  useEffect(() => {
+    if (autoScroll && progress?.logs.length) {
+      const logsContainer = document.getElementById('logs-container');
+      if (logsContainer) {
+        logsContainer.scrollTop = logsContainer.scrollHeight;
+      }
+    }
+  }, [progress?.logs, autoScroll]);
+
+  const handleMigrate = async () => {
+    if (!oldRedisUrl.trim()) {
+      setError('Please enter the old Redis URL');
+      return;
+    }
+
+    setMigrating(true);
+    setError(null);
+    setProgress(null);
+    setMigrationId(null);
+
+    try {
+      const response = await fetch('/api/admin/migrate-redis', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ oldRedisUrl, dryRun }),
+      });
+
+      const data = await response.json();
+
+      if (response.ok) {
+        setMigrationId(data.migrationId);
+      } else {
+        setError(data.error || 'Failed to start migration');
+        setMigrating(false);
+      }
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Unknown error');
+      setMigrating(false);
+    }
+  };
+
+  const progressPercent = progress
+    ? Math.round((progress.progress / progress.total) * 100)
+    : 0;
+
+  return (
+    <div className="p-6 md:p-8 max-w-4xl mx-auto">
+      {/* Header */}
+      <div className="bg-card border border-border rounded-xl p-6 mb-6">
+        <div className="flex items-start gap-4">
+          <div className="flex-shrink-0 w-12 h-12 bg-primary/20 rounded-full flex items-center justify-center text-2xl">
+            🔄
+          </div>
+          <div className="flex-1">
+            <h1 className="text-3xl font-bold text-foreground mb-2">
+              Redis Migration
+            </h1>
+            <p className="text-muted-foreground">
+              Migrate all data from your old Redis instance to the new one.
+              This will copy all keys while preserving types and TTLs.
+            </p>
+          </div>
+        </div>
+      </div>
+
+      {/* Migration Form */}
+      {!migrating && !progress && (
+        <div className="bg-card border border-border rounded-xl p-6 mb-6">
+          <h2 className="text-xl font-bold text-foreground mb-4">
+            Configuration
+          </h2>
+
+          <div className="space-y-4">
+            {/* Old Redis URL */}
+            <div>
+              <label
+                htmlFor="oldRedisUrl"
+                className="block text-sm font-medium text-foreground mb-2"
+              >
+                Old Redis URL
+              </label>
+              <input
+                id="oldRedisUrl"
+                type="text"
+                value={oldRedisUrl}
+                onChange={(e) => setOldRedisUrl(e.target.value)}
+                placeholder="redis://username:password@host:port"
+                className="w-full px-4 py-3 bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
+              />
+              <p className="text-xs text-muted-foreground mt-1">
+                Format: redis://[username:password@]host:port[/database]
+              </p>
+            </div>
+
+            {/* Dry Run Option */}
+            <div className="flex items-start gap-3 p-4 bg-muted rounded-lg">
+              <input
+                id="dryRun"
+                type="checkbox"
+                checked={dryRun}
+                onChange={(e) => setDryRun(e.target.checked)}
+                className="mt-1 w-4 h-4 accent-primary"
+              />
+              <div>
+                <label
+                  htmlFor="dryRun"
+                  className="text-sm font-medium text-foreground cursor-pointer"
+                >
+                  Dry Run (Recommended)
+                </label>
+                <p className="text-xs text-muted-foreground mt-1">
+                  Test the migration without actually copying data. This will
+                  show you what would be migrated without making any changes.
+                </p>
+              </div>
+            </div>
+
+            {/* Info Box */}
+            <div className="bg-primary/10 border border-primary/20 rounded-lg p-4">
+              <p className="text-sm font-medium text-foreground mb-2">
+                ℹ️ Migration Details
+              </p>
+              <ul className="text-xs text-muted-foreground space-y-1 list-disc list-inside">
+                <li>Scans all keys from the old Redis instance</li>
+                <li>Preserves key types (string, hash, list, set, zset)</li>
+                <li>Maintains TTL (expiration) values</li>
+                <li>Skips keys that already exist in the new instance</li>
+                <li>Provides real-time progress updates</li>
+              </ul>
+            </div>
+
+            {/* Error Display */}
+            {error && (
+              <div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4">
+                <p className="text-sm font-medium text-destructive">
+                  ❌ {error}
+                </p>
+              </div>
+            )}
+
+            {/* Start Migration Button */}
+            <button
+              onClick={handleMigrate}
+              disabled={!oldRedisUrl.trim()}
+              className="w-full bg-primary text-primary-foreground px-6 py-3 rounded-lg font-semibold hover:bg-primary/90 transition disabled:opacity-50 disabled:cursor-not-allowed"
+            >
+              {dryRun ? '🔍 Start Dry Run' : '🚀 Start Migration'}
+            </button>
+          </div>
+        </div>
+      )}
+
+      {/* Migration Progress */}
+      {migrating && progress && (
+        <div className="bg-card border border-border rounded-xl p-6 mb-6">
+          <h2 className="text-xl font-bold text-foreground mb-4">
+            {progress.status === 'running' && '⏳ Migration in Progress'}
+            {progress.status === 'completed' && '✅ Migration Completed'}
+            {progress.status === 'failed' && '❌ Migration Failed'}
+          </h2>
+
+          {/* Progress Bar */}
+          <div className="mb-6">
+            <div className="flex items-center justify-between mb-2">
+              <span className="text-sm text-muted-foreground">
+                {progress.progress} / {progress.total} keys
+              </span>
+              <span className="text-sm font-medium text-foreground">
+                {progressPercent}%
+              </span>
+            </div>
+            <div className="w-full h-3 bg-muted rounded-full overflow-hidden">
+              <div
+                className="h-full bg-primary transition-all duration-300"
+                style={{ width: `${progressPercent}%` }}
+              />
+            </div>
+            {progress.currentKey && (
+              <p className="text-xs text-muted-foreground mt-2 truncate">
+                Current: {progress.currentKey}
+              </p>
+            )}
+          </div>
+
+          {/* Stats */}
+          <div className="grid grid-cols-3 gap-4 mb-6">
+            <StatBox
+              label="Migrated"
+              value={progress.migratedKeys}
+              color="text-green-600 dark:text-green-400"
+            />
+            <StatBox
+              label="Skipped"
+              value={progress.skippedKeys}
+              color="text-yellow-600 dark:text-yellow-400"
+            />
+            <StatBox
+              label="Failed"
+              value={progress.failedKeys}
+              color="text-red-600 dark:text-red-400"
+            />
+          </div>
+
+          {/* Live Logs */}
+          {progress.logs.length > 0 && (
+            <div className="bg-muted border border-border rounded-lg overflow-hidden">
+              <div className="flex items-center justify-between p-3 border-b border-border bg-card">
+                <p className="text-sm font-medium text-foreground">
+                  📝 Migration Logs ({progress.logs.length})
+                </p>
+                <label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
+                  <input
+                    type="checkbox"
+                    checked={autoScroll}
+                    onChange={(e) => setAutoScroll(e.target.checked)}
+                    className="w-4 h-4 accent-primary"
+                  />
+                  Auto-scroll
+                </label>
+              </div>
+              <div
+                id="logs-container"
+                className="max-h-64 overflow-y-auto p-3 space-y-1 font-mono text-xs bg-background"
+              >
+                {progress.logs.map((log, i) => (
+                  <div
+                    key={i}
+                    className={`${
+                      log.includes('❌')
+                        ? 'text-red-600 dark:text-red-400'
+                        : log.includes('⚠️')
+                        ? 'text-yellow-600 dark:text-yellow-400'
+                        : 'text-muted-foreground'
+                    }`}
+                  >
+                    {log}
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+
+          {/* Errors Summary */}
+          {progress.errors.length > 0 && (
+            <div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4">
+              <p className="text-sm font-medium text-destructive mb-2">
+                ❌ Error Summary ({progress.errors.length})
+              </p>
+              <div className="max-h-32 overflow-y-auto space-y-1">
+                {progress.errors.map((err, i) => (
+                  <p key={i} className="text-xs text-muted-foreground font-mono">
+                    {err}
+                  </p>
+                ))}
+              </div>
+            </div>
+          )}
+
+          {/* Completion Info */}
+          {progress.status === 'completed' && (
+            <div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 mt-4">
+              <p className="text-sm font-medium text-green-600 dark:text-green-400">
+                ✅ Migration completed successfully!
+              </p>
+              <p className="text-xs text-muted-foreground mt-1">
+                Started: {new Date(progress.startedAt).toLocaleString()}
+                <br />
+                Completed: {progress.completedAt && new Date(progress.completedAt).toLocaleString()}
+              </p>
+            </div>
+          )}
+
+          {/* Actions */}
+          {progress.status !== 'running' && (
+            <button
+              onClick={() => {
+                setProgress(null);
+                setMigrationId(null);
+                setMigrating(false);
+                setOldRedisUrl('');
+              }}
+              className="w-full bg-secondary text-secondary-foreground px-6 py-3 rounded-lg font-semibold hover:bg-secondary/90 transition mt-4"
+            >
+              Start New Migration
+            </button>
+          )}
+        </div>
+      )}
+
+      {/* Info Panel */}
+      <div className="bg-muted border border-border rounded-xl p-6">
+        <h3 className="text-lg font-bold text-foreground mb-3">
+          Migration Guide
+        </h3>
+        <div className="space-y-2 text-sm text-muted-foreground">
+          <p>
+            <strong className="text-foreground">1. Test First:</strong> Always
+            run a dry run first to see what will be migrated.
+          </p>
+          <p>
+            <strong className="text-foreground">2. Check Results:</strong>{' '}
+            Review the dry run results for any errors or unexpected keys.
+          </p>
+          <p>
+            <strong className="text-foreground">3. Live Migration:</strong>{' '}
+            Once satisfied, uncheck "Dry Run" and migrate for real.
+          </p>
+          <p>
+            <strong className="text-foreground">4. Verification:</strong> After
+            migration, verify your application is working correctly.
+          </p>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+function StatBox({
+  label,
+  value,
+  color,
+}: {
+  label: string;
+  value: number;
+  color: string;
+}) {
+  return (
+    <div className="bg-muted border border-border rounded-lg p-4 text-center">
+      <div className={`text-2xl font-bold ${color} mb-1`}>{value}</div>
+      <div className="text-xs text-muted-foreground">{label}</div>
+    </div>
+  );
+}
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index bcaad16..25c920f 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -22,6 +22,12 @@ export default function AdminHomePage() {
             title="Data Overview"
             description="View system stats and data sources"
           />
+          <QuickAction
+            href="/admin/migrate-redis"
+            icon="🔄"
+            title="Migrate Redis"
+            description="Move data from old to new Redis"
+          />
           <QuickAction
             href="/admin/reset"
             icon="⚠️"
diff --git a/src/app/api/admin/migrate-redis/route.ts b/src/app/api/admin/migrate-redis/route.ts
new file mode 100644
index 0000000..d38f6ed
--- /dev/null
+++ b/src/app/api/admin/migrate-redis/route.ts
@@ -0,0 +1,350 @@
+/**
+ * Redis Migration API
+ * Migrates all data from an old Redis instance to the new one
+ */
+
+import { NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { UserManagementService } from '@/lib/admin/user-management-service';
+import Redis from 'ioredis';
+import { getRedisClient } from '@/lib/redis';
+import { logger } from '@/lib/logger';
+
+export const dynamic = 'force-dynamic';
+
+interface MigrationProgress {
+  status: 'running' | 'completed' | 'failed';
+  progress: number;
+  total: number;
+  currentKey?: string;
+  migratedKeys: number;
+  skippedKeys: number;
+  failedKeys: number;
+  errors: string[];
+  logs: string[];
+  startedAt: string;
+  completedAt?: string;
+}
+
+// Store migration progress in memory (could be moved to Redis if needed)
+const migrationProgress = new Map<string, MigrationProgress>();
+
+export async function POST(request: Request) {
+  try {
+    // Check authentication and admin status
+    const session = await getServerSession(authOptions);
+    if (!session?.user?.email) {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const isAdmin = await UserManagementService.isAdmin(session.user.email);
+    if (!isAdmin) {
+      return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
+    }
+
+    const body = await request.json();
+    const { oldRedisUrl, dryRun = false } = body;
+
+    if (!oldRedisUrl || typeof oldRedisUrl !== 'string') {
+      return NextResponse.json(
+        { error: 'oldRedisUrl is required and must be a string' },
+        { status: 400 }
+      );
+    }
+
+    // Generate migration ID
+    const migrationId = `migration-${Date.now()}`;
+
+    // Initialize progress tracking
+    migrationProgress.set(migrationId, {
+      status: 'running',
+      progress: 0,
+      total: 0,
+      migratedKeys: 0,
+      skippedKeys: 0,
+      failedKeys: 0,
+      errors: [],
+      logs: [],
+      startedAt: new Date().toISOString(),
+    });
+
+    // Start migration in background
+    performMigration(migrationId, oldRedisUrl, dryRun, session.user.email).catch((error) => {
+      logger.error('Migration failed:', error);
+      const progress = migrationProgress.get(migrationId);
+      if (progress) {
+        progress.status = 'failed';
+        progress.errors.push(error.message || 'Unknown error');
+        progress.completedAt = new Date().toISOString();
+      }
+    });
+
+    return NextResponse.json({
+      migrationId,
+      message: 'Migration started',
+      dryRun,
+    });
+  } catch (error) {
+    logger.error('Error starting migration:', error);
+    return NextResponse.json(
+      { error: error instanceof Error ? error.message : 'Failed to start migration' },
+      { status: 500 }
+    );
+  }
+}
+
+export async function GET(request: Request) {
+  try {
+    // Check authentication and admin status
+    const session = await getServerSession(authOptions);
+    if (!session?.user?.email) {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const isAdmin = await UserManagementService.isAdmin(session.user.email);
+    if (!isAdmin) {
+      return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
+    }
+
+    const { searchParams } = new URL(request.url);
+    const migrationId = searchParams.get('migrationId');
+
+    if (!migrationId) {
+      return NextResponse.json(
+        { error: 'migrationId is required' },
+        { status: 400 }
+      );
+    }
+
+    const progress = migrationProgress.get(migrationId);
+    if (!progress) {
+      return NextResponse.json(
+        { error: 'Migration not found' },
+        { status: 404 }
+      );
+    }
+
+    return NextResponse.json(progress);
+  } catch (error) {
+    logger.error('Error getting migration status:', error);
+    return NextResponse.json(
+      { error: error instanceof Error ? error.message : 'Failed to get migration status' },
+      { status: 500 }
+    );
+  }
+}
+
+async function performMigration(
+  migrationId: string,
+  oldRedisUrl: string,
+  dryRun: boolean,
+  userEmail: string
+) {
+  let oldRedis: Redis | null = null;
+  let newRedis: Redis | null = null;
+
+  // Helper to add logs to progress
+  const addLog = (message: string, level: 'info' | 'warn' | 'error' = 'info') => {
+    const progress = migrationProgress.get(migrationId);
+    if (progress) {
+      const timestamp = new Date().toLocaleTimeString();
+      const icon = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : 'ℹ️';
+      progress.logs.push(`[${timestamp}] ${icon} ${message}`);
+      // Keep only last 100 logs
+      if (progress.logs.length > 100) {
+        progress.logs.shift();
+      }
+    }
+    // Also log to console
+    if (level === 'error') {
+      logger.error(message);
+    } else if (level === 'warn') {
+      logger.warn(message);
+    } else {
+      logger.info(message);
+    }
+  };
+
+  try {
+    addLog(`🚀 Starting Redis migration (${dryRun ? 'DRY RUN' : 'LIVE'}) by ${userEmail}`);
+
+    // Connect to old Redis
+    addLog('Connecting to old Redis instance...');
+    oldRedis = new Redis(oldRedisUrl, {
+      maxRetriesPerRequest: 3,
+      retryStrategy(times) {
+        const delay = Math.min(times * 50, 2000);
+        return delay;
+      },
+    });
+    addLog('✅ Connected to old Redis');
+
+    // Connect to new Redis
+    addLog('Connecting to new Redis instance...');
+    newRedis = getRedisClient();
+    addLog('✅ Connected to new Redis');
+
+    // Get all keys from old Redis
+    addLog('Scanning for keys in old Redis...');
+    const keys = await scanAllKeys(oldRedis);
+    const progress = migrationProgress.get(migrationId);
+    if (!progress) throw new Error('Migration progress not found');
+
+    progress.total = keys.length;
+    addLog(`📊 Found ${keys.length} keys to migrate`);
+
+    // Migrate each key
+    addLog(`Starting migration of ${keys.length} keys...`);
+    for (let i = 0; i < keys.length; i++) {
+      const key = keys[i];
+      progress.currentKey = key;
+      progress.progress = i + 1;
+
+      try {
+        // Get key type
+        const type = await oldRedis.type(key);
+
+        if (dryRun) {
+          addLog(`[DRY RUN] Would migrate ${type} key: ${key}`);
+          progress.migratedKeys++;
+          continue;
+        }
+
+        // Check if key already exists in new Redis
+        const exists = await newRedis.exists(key);
+        if (exists) {
+          addLog(`⏭️ Skipping existing key: ${key}`, 'warn');
+          progress.skippedKeys++;
+          continue;
+        }
+
+        // Migrate based on type
+        switch (type) {
+          case 'string': {
+            const value = await oldRedis.get(key);
+            if (value !== null) {
+              const ttl = await oldRedis.ttl(key);
+              if (ttl > 0) {
+                await newRedis.setex(key, ttl, value);
+              } else {
+                await newRedis.set(key, value);
+              }
+            }
+            break;
+          }
+
+          case 'hash': {
+            const hash = await oldRedis.hgetall(key);
+            if (Object.keys(hash).length > 0) {
+              await newRedis.hset(key, hash);
+              const ttl = await oldRedis.ttl(key);
+              if (ttl > 0) {
+                await newRedis.expire(key, ttl);
+              }
+            }
+            break;
+          }
+
+          case 'list': {
+            const list = await oldRedis.lrange(key, 0, -1);
+            if (list.length > 0) {
+              await newRedis.rpush(key, ...list);
+              const ttl = await oldRedis.ttl(key);
+              if (ttl > 0) {
+                await newRedis.expire(key, ttl);
+              }
+            }
+            break;
+          }
+
+          case 'set': {
+            const set = await oldRedis.smembers(key);
+            if (set.length > 0) {
+              await newRedis.sadd(key, ...set);
+              const ttl = await oldRedis.ttl(key);
+              if (ttl > 0) {
+                await newRedis.expire(key, ttl);
+              }
+            }
+            break;
+          }
+
+          case 'zset': {
+            const zset = await oldRedis.zrange(key, 0, -1, 'WITHSCORES');
+            if (zset.length > 0) {
+              // Convert array to score-member pairs
+              const pairs: (string | number)[] = [];
+              for (let j = 0; j < zset.length; j += 2) {
+                pairs.push(parseFloat(zset[j + 1]), zset[j]);
+              }
+              await newRedis.zadd(key, ...pairs);
+              const ttl = await oldRedis.ttl(key);
+              if (ttl > 0) {
+                await newRedis.expire(key, ttl);
+              }
+            }
+            break;
+          }
+
+          default:
+            addLog(`⚠️ Unknown key type: ${type} for key: ${key}`, 'warn');
+            progress.skippedKeys++;
+            continue;
+        }
+
+        progress.migratedKeys++;
+        // Only log every 10th key to avoid spam, or if it's the last key
+        if (i % 10 === 0 || i === keys.length - 1) {
+          addLog(`✅ Migrated ${type} key: ${key} (${i + 1}/${keys.length})`);
+        }
+      } catch (error) {
+        const errorMsg = error instanceof Error ? error.message : 'Unknown error';
+        addLog(`❌ Failed to migrate key ${key}: ${errorMsg}`, 'error');
+        progress.failedKeys++;
+        progress.errors.push(`${key}: ${errorMsg}`);
+      }
+    }
+
+    // Mark as completed
+    progress.status = 'completed';
+    progress.completedAt = new Date().toISOString();
+    progress.currentKey = undefined;
+
+    addLog(`✅ Migration completed! Total: ${progress.total}, Migrated: ${progress.migratedKeys}, Skipped: ${progress.skippedKeys}, Failed: ${progress.failedKeys}`);
+
+  } catch (error) {
+    const errorMsg = error instanceof Error ? error.message : 'Unknown error';
+    logger.error('Migration failed:', error);
+    const progress = migrationProgress.get(migrationId);
+    if (progress) {
+      progress.status = 'failed';
+      progress.errors.push(errorMsg);
+      progress.completedAt = new Date().toISOString();
+      const timestamp = new Date().toLocaleTimeString();
+      progress.logs.push(`[${timestamp}] ❌ Migration failed: ${errorMsg}`);
+    }
+    throw error;
+  } finally {
+    // Clean up old Redis connection
+    if (oldRedis) {
+      await oldRedis.quit();
+    }
+  }
+}
+
+/**
+ * Scan all keys in Redis using SCAN to avoid blocking
+ */
+async function scanAllKeys(redis: Redis): Promise<string[]> {
+  const keys: string[] = [];
+  let cursor = '0';
+
+  do {
+    const result = await redis.scan(cursor, 'COUNT', 100);
+    cursor = result[0];
+    keys.push(...result[1]);
+  } while (cursor !== '0');
+
+  return keys;
+}
