diff --git a/.gitignore b/.gitignore index 4034d318..6c4b7bcf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ workspace/* .opencode +data/ .trae/ /backend/database/migrations/* diff --git a/fix_database.py b/fix_database.py new file mode 100644 index 00000000..fec59cb8 --- /dev/null +++ b/fix_database.py @@ -0,0 +1,74 @@ +""" +Quick fix for missing wavespeed columns in usage_summaries table +Run this script to fix the database schema issue +""" + +import sqlite3 +import os + +def fix_database(): + # Find database file + db_path = None + for path in ["backend/database.db", "database.db"]: + if os.path.exists(path): + db_path = path + break + + if not db_path: + print("❌ Database not found!") + print("Please make sure you're running this from the project root directory") + return + + print(f"📁 Using database: {db_path}") + + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Check if table exists + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='usage_summaries'") + if not cursor.fetchone(): + print("❌ Table 'usage_summaries' not found!") + return + + # Get current columns + cursor.execute("PRAGMA table_info(usage_summaries)") + columns = [row[1] for row in cursor.fetchall()] + + # Columns that need to be added + missing_columns = [] + required_columns = [ + 'wavespeed_calls', 'tavily_calls', 'serper_calls', 'metaphor_calls', + 'firecrawl_calls', 'stability_calls', 'exa_calls', 'video_calls', + 'image_edit_calls', 'audio_calls', 'wavespeed_tokens', 'wavespeed_cost', + 'tavily_cost', 'serper_cost', 'metaphor_cost', 'firecrawl_cost', + 'stability_cost', 'exa_cost', 'video_cost', 'image_edit_cost', 'audio_cost' + ] + + for col in required_columns: + if col not in columns: + missing_columns.append(col) + + if missing_columns: + print(f"➕ Adding {len(missing_columns)} missing columns...") + for col in missing_columns: + if col.endswith('_calls') or col.endswith('_tokens'): + cursor.execute(f"ALTER TABLE usage_summaries ADD COLUMN {col} INTEGER DEFAULT 0") + else: # cost columns + cursor.execute(f"ALTER TABLE usage_summaries ADD COLUMN {col} FLOAT DEFAULT 0.0") + print(f" ✅ Added {col}") + + conn.commit() + print("🎉 Database schema updated successfully!") + else: + print("✅ All columns already exist!") + + conn.close() + + except Exception as e: + print(f"❌ Error: {e}") + +if __name__ == "__main__": + print("🔧 Fixing database schema for usage_summaries...") + fix_database() + print("✅ Done!") diff --git a/frontend/src/components/PodcastMaker/RobustCamera.tsx b/frontend/src/components/PodcastMaker/RobustCamera.tsx new file mode 100644 index 00000000..0a2108a5 --- /dev/null +++ b/frontend/src/components/PodcastMaker/RobustCamera.tsx @@ -0,0 +1,406 @@ +import React, { useState, useRef, useCallback, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Box, + Typography, + CircularProgress, + Alert, + IconButton, + Tooltip +} from '@mui/material'; +import { + CameraAlt, + FlipCameraAndroid, + Close, + Camera +} from '@mui/icons-material'; + +interface RobustCameraProps { + onCapture: (imageDataUrl: string) => void; + onClose: () => void; + open: boolean; +} + +export const RobustCamera: React.FC = ({ onCapture, onClose, open }) => { + const [stream, setStream] = useState(null); + const [facingMode, setFacingMode] = useState<'user' | 'environment'>('user'); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [cameraReady, setCameraReady] = useState(false); + + // Use multiple refs for different purposes + const videoElementRef = useRef(null); + const canvasRef = useRef(null); + const streamRef = useRef(null); + const cameraInitRef = useRef(false); + const retryCountRef = useRef(0); + + // Clean up stream + const cleanupStream = useCallback(() => { + console.log('[RobustCamera] Cleaning up stream'); + if (streamRef.current) { + streamRef.current.getTracks().forEach(track => track.stop()); + streamRef.current = null; + } + if (videoElementRef.current && videoElementRef.current.srcObject) { + videoElementRef.current.srcObject = null; + } + setStream(null); + setCameraReady(false); + cameraInitRef.current = false; + retryCountRef.current = 0; + }, []); + + // Initialize camera + const initializeCamera = useCallback(async () => { + if (cameraInitRef.current || loading) { + console.log('[RobustCamera] Camera already initializing or loading'); + return; + } + + console.log('[RobustCamera] Starting camera initialization'); + cameraInitRef.current = true; + setLoading(true); + setError(null); + cleanupStream(); + + try { + const constraints = { + video: { + facingMode: facingMode, + width: { ideal: 1280 }, + height: { ideal: 720 }, + }, + audio: false, + }; + + console.log('[RobustCamera] Requesting camera with constraints:', constraints); + const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + console.log('[RobustCamera] Camera stream obtained successfully'); + + streamRef.current = mediaStream; + setStream(mediaStream); + + // Attach to video element + if (videoElementRef.current) { + console.log('[RobustCamera] Video element found, attaching stream'); + videoElementRef.current.srcObject = mediaStream; + + videoElementRef.current.onloadedmetadata = () => { + console.log('[RobustCamera] Video metadata loaded'); + setCameraReady(true); + setLoading(false); + videoElementRef.current?.play().catch(err => { + console.error('[RobustCamera] Video play error:', err); + setError('Camera stream obtained but video display failed. Please try again.'); + }); + }; + + videoElementRef.current.onerror = (err) => { + console.error('[RobustCamera] Video error:', err); + setError('Failed to display camera feed.'); + setLoading(false); + }; + } else { + console.log('[RobustCamera] Video element not found, will attach when ready'); + setCameraReady(false); + setLoading(false); + // Stream will be attached when video element mounts + } + + } catch (err) { + console.error('[RobustCamera] Camera access error:', err); + cleanupStream(); + setLoading(false); + + if (err instanceof Error) { + if (err.name === 'NotAllowedError') { + setError('Camera access denied. Please allow camera permissions to take a selfie.'); + } else if (err.name === 'NotFoundError') { + setError('No camera found on this device.'); + } else if (err.name === 'NotReadableError') { + setError('Camera is already in use by another application.'); + } else { + setError('Failed to access camera. Please try again.'); + } + } + } + }, [facingMode, loading, cleanupStream]); + + // Attach stream when video element is available + const attachStreamToVideo = useCallback(() => { + if (videoElementRef.current && streamRef.current && !cameraReady) { + console.log('[RobustCamera] Attaching stream to video element'); + const video = videoElementRef.current; + const stream = streamRef.current; + + // Clear any existing stream + if (video.srcObject) { + const oldStream = video.srcObject as MediaStream; + oldStream.getTracks().forEach(track => track.stop()); + } + + // Attach new stream + video.srcObject = stream; + + video.onloadedmetadata = () => { + console.log('[RobustCamera] Video metadata loaded after attachment'); + setCameraReady(true); + setLoading(false); + video.play().catch(err => { + console.error('[RobustCamera] Video play error after attachment:', err); + setError('Camera stream obtained but video display failed. Please try again.'); + }); + }; + + video.onerror = (err) => { + console.error('[RobustCamera] Video error after attachment:', err); + setError('Failed to display camera feed.'); + setLoading(false); + }; + } else { + console.log('[RobustCamera] Cannot attach stream:', { + videoExists: !!videoElementRef.current, + streamExists: !!streamRef.current, + cameraReady + }); + } + }, [cameraReady]); + + // Capture photo + const capturePhoto = useCallback(() => { + if (!videoElementRef.current || !canvasRef.current || !cameraReady) { + console.log('[RobustCamera] Cannot capture: video or canvas not ready'); + return; + } + + const video = videoElementRef.current; + const canvas = canvasRef.current; + + // Set canvas dimensions to match video + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // Draw video frame to canvas + const context = canvas.getContext('2d'); + if (context) { + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Convert to data URL + const imageDataUrl = canvas.toDataURL('image/jpeg', 0.9); + console.log('[RobustCamera] Photo captured successfully'); + onCapture(imageDataUrl); + onClose(); + } + }, [cameraReady, onCapture, onClose]); + + // Flip camera + const flipCamera = useCallback(() => { + console.log('[RobustCamera] Flipping camera'); + setFacingMode(prev => prev === 'user' ? 'environment' : 'user'); + }, []); + + // Initialize camera when dialog opens + useEffect(() => { + if (open) { + console.log('[RobustCamera] Dialog opened, initializing camera'); + // Small delay to ensure DOM is ready + const timer = setTimeout(() => { + initializeCamera(); + }, 300); + + return () => { + clearTimeout(timer); + cleanupStream(); + }; + } + }, [open]); // Remove initializeCamera and cleanupStream from dependencies + + // Re-initialize when facing mode changes + useEffect(() => { + if (open && cameraReady) { + console.log('[RobustCamera] Facing mode changed, re-initializing camera'); + cleanupStream(); + const timer = setTimeout(() => { + initializeCamera(); + }, 500); + + return () => clearTimeout(timer); + } + }, [facingMode]); // Remove other dependencies to prevent loops + + // Attach stream when video element is available + useEffect(() => { + if (videoElementRef.current && streamRef.current && !cameraReady) { + console.log('[RobustCamera] Video element available, attaching stream'); + attachStreamToVideo(); + } + }, [stream, cameraReady]); // Trigger when stream changes + + // Attach stream when component mounts or stream changes + useEffect(() => { + if (open && stream && !cameraReady && videoElementRef.current) { + console.log('[RobustCamera] Stream available, attaching to video element'); + attachStreamToVideo(); + } + }, [open, stream]); // Remove cameraReady and attachStreamToVideo to prevent loops + + return ( + + + + + Take a Selfie + + + + + + + + {error && ( + + {error} + + )} + + {loading && ( + + + + Initializing camera... + + + )} + + {!loading && !error && ( + + + )} + + + + + + {cameraReady && ( + <> + + + + + + + + )} + + + + + ); +}; diff --git a/frontend/src/utils/mediaCache.ts b/frontend/src/utils/mediaCache.ts new file mode 100644 index 00000000..c223b5d1 --- /dev/null +++ b/frontend/src/utils/mediaCache.ts @@ -0,0 +1,357 @@ +/** + * Media Cache Utility + * + * Provides intelligent caching for images, videos, and audio to prevent + * unnecessary network requests when navigating between phases. + * + * Enhanced with scene-specific cache keys to prevent cross-contamination + * between different podcasts and scenes. + */ + +interface CacheEntry { + blobUrl: string; + timestamp: number; + originalUrl: string; + mediaType: 'image' | 'video' | 'audio'; + size?: number; + sceneId?: string; + projectId?: string; +} + +class MediaCache { + private cache = new Map(); + private readonly maxAge = 10 * 60 * 1000; // 10 minutes + private readonly maxSize = 50; // Maximum number of cached items + private blobCleanupMap = new Map(); // Maps blobUrl to cache key + + /** + * Generate a unique cache key that includes scene and project context + */ + private generateCacheKey(url: string, sceneId?: string, projectId?: string): string { + // Create a composite key that prevents cross-contamination + const parts = [url]; + if (sceneId) parts.push(`scene:${sceneId}`); + if (projectId) parts.push(`project:${projectId}`); + return parts.join('|'); + } + + /** + * Extract original URL from cache key + */ + private extractOriginalUrl(cacheKey: string): string { + return cacheKey.split('|')[0]; + } + + /** + * Get cached blob URL for a media resource with optional scene context + */ + get(url: string, sceneId?: string, projectId?: string): string | null { + const cacheKey = this.generateCacheKey(url, sceneId, projectId); + const entry = this.cache.get(cacheKey); + + if (!entry) { + // Try without scene context for backwards compatibility + const fallbackKey = this.generateCacheKey(url); + const fallbackEntry = this.cache.get(fallbackKey); + if (fallbackEntry) { + console.log(`[MediaCache] Cache hit (fallback) for ${fallbackEntry.mediaType}:`, url); + return fallbackEntry.blobUrl; + } + return null; + } + + // Check if cache entry is still valid + if (Date.now() - entry.timestamp > this.maxAge) { + this.invalidate(cacheKey); + return null; + } + + console.log(`[MediaCache] Cache hit for ${entry.mediaType}:`, url, + sceneId ? `(scene: ${sceneId})` : '', + projectId ? `(project: ${projectId})` : ''); + return entry.blobUrl; + } + + /** + * Set cached blob URL for a media resource with scene context + */ + set(url: string, blobUrl: string, mediaType: 'image' | 'video' | 'audio', size?: number, sceneId?: string, projectId?: string): void { + const cacheKey = this.generateCacheKey(url, sceneId, projectId); + + // Clean up existing blob URL if it exists + const existingEntry = this.cache.get(cacheKey); + if (existingEntry && existingEntry.blobUrl !== blobUrl) { + this.revokeBlob(existingEntry.blobUrl); + } + + const entry: CacheEntry = { + blobUrl, + timestamp: Date.now(), + originalUrl: url, + mediaType, + size, + sceneId, + projectId + }; + + this.cache.set(cacheKey, entry); + this.blobCleanupMap.set(blobUrl, cacheKey); + + // Enforce cache size limit + if (this.cache.size > this.maxSize) { + this.evictOldest(); + } + + console.log(`[MediaCache] Cached ${mediaType}:`, url, + sceneId ? `(scene: ${sceneId})` : '', + projectId ? `(project: ${projectId})` : ''); + } + + /** + * Check if URL is cached (with optional scene context) + */ + has(url: string, sceneId?: string, projectId?: string): boolean { + const cacheKey = this.generateCacheKey(url, sceneId, projectId); + const entry = this.cache.get(cacheKey); + if (!entry) return false; + + // Check if still valid + if (Date.now() - entry.timestamp > this.maxAge) { + this.invalidate(cacheKey); + return false; + } + + return true; + } + + /** + * Invalidate cache entry for a specific URL (with optional scene context) + */ + invalidate(url: string, sceneId?: string, projectId?: string): void { + const cacheKey = this.generateCacheKey(url, sceneId, projectId); + const entry = this.cache.get(cacheKey); + if (entry) { + this.revokeBlob(entry.blobUrl); + this.cache.delete(cacheKey); + this.blobCleanupMap.delete(entry.blobUrl); + } + } + + /** + * Clear all cache + */ + clear(): void { + // Revoke all blob URLs + for (const entry of this.cache.values()) { + this.revokeBlob(entry.blobUrl); + } + + this.cache.clear(); + this.blobCleanupMap.clear(); + console.log('[MediaCache] Cache cleared'); + } + + /** + * Clear cache for specific scene + */ + clearScene(sceneId: string): void { + const toDelete: string[] = []; + + for (const [cacheKey, entry] of this.cache.entries()) { + if (entry.sceneId === sceneId) { + toDelete.push(cacheKey); + } + } + + toDelete.forEach(cacheKey => { + const entry = this.cache.get(cacheKey); + if (entry) { + this.revokeBlob(entry.blobUrl); + this.cache.delete(cacheKey); + this.blobCleanupMap.delete(entry.blobUrl); + } + }); + + if (toDelete.length > 0) { + console.log(`[MediaCache] Cleared ${toDelete.length} cache entries for scene: ${sceneId}`); + } + } + + /** + * Clear cache for specific project + */ + clearProject(projectId: string): void { + const toDelete: string[] = []; + + for (const [cacheKey, entry] of this.cache.entries()) { + if (entry.projectId === projectId) { + toDelete.push(cacheKey); + } + } + + toDelete.forEach(cacheKey => { + const entry = this.cache.get(cacheKey); + if (entry) { + this.revokeBlob(entry.blobUrl); + this.cache.delete(cacheKey); + this.blobCleanupMap.delete(entry.blobUrl); + } + }); + + if (toDelete.length > 0) { + console.log(`[MediaCache] Cleared ${toDelete.length} cache entries for project: ${projectId}`); + } + } + + /** + * Get cache statistics + */ + getStats() { + const stats = { + totalEntries: this.cache.size, + entriesByType: {} as Record, + entriesByScene: {} as Record, + entriesByProject: {} as Record, + totalSize: 0, + oldestEntry: 0, + newestEntry: 0 + }; + + let oldest = Date.now(); + let newest = 0; + + for (const entry of this.cache.values()) { + // Count by type + stats.entriesByType[entry.mediaType] = (stats.entriesByType[entry.mediaType] || 0) + 1; + + // Count by scene + if (entry.sceneId) { + stats.entriesByScene[entry.sceneId] = (stats.entriesByScene[entry.sceneId] || 0) + 1; + } + + // Count by project + if (entry.projectId) { + stats.entriesByProject[entry.projectId] = (stats.entriesByProject[entry.projectId] || 0) + 1; + } + + // Sum sizes + if (entry.size) { + stats.totalSize += entry.size; + } + + // Track ages + oldest = Math.min(oldest, entry.timestamp); + newest = Math.max(newest, entry.timestamp); + } + + stats.oldestEntry = oldest > 0 ? Date.now() - oldest : 0; + stats.newestEntry = newest > 0 ? Date.now() - newest : 0; + + return stats; + } + + /** + * Clean up expired entries + */ + cleanup(): void { + const now = Date.now(); + const toDelete: string[] = []; + + for (const [cacheKey, entry] of this.cache.entries()) { + if (now - entry.timestamp > this.maxAge) { + toDelete.push(cacheKey); + } + } + + toDelete.forEach(cacheKey => this.invalidate(this.extractOriginalUrl(cacheKey))); + + if (toDelete.length > 0) { + console.log(`[MediaCache] Cleaned up ${toDelete.length} expired entries`); + } + } + + /** + * Revoke blob URL safely + */ + private revokeBlob(blobUrl: string): void { + try { + if (blobUrl.startsWith('blob:')) { + URL.revokeObjectURL(blobUrl); + } + } catch (error) { + console.warn('[MediaCache] Failed to revoke blob URL:', error); + } + } + + /** + * Evict oldest cache entry + */ + private evictOldest(): void { + let oldestKey = ''; + let oldestTime = Date.now(); + + for (const [cacheKey, entry] of this.cache.entries()) { + if (entry.timestamp < oldestTime) { + oldestTime = entry.timestamp; + oldestKey = cacheKey; + } + } + + if (oldestKey) { + const entry = this.cache.get(oldestKey); + if (entry) { + console.log('[MediaCache] Evicted oldest entry:', this.extractOriginalUrl(oldestKey), + entry.sceneId ? `(scene: ${entry.sceneId})` : ''); + } + this.invalidate(this.extractOriginalUrl(oldestKey)); + } + } +} + +// Export singleton instance +export const mediaCache = new MediaCache(); + +// Set up periodic cleanup +setInterval(() => { + mediaCache.cleanup(); +}, 5 * 60 * 1000); // Clean up every 5 minutes + +// Export utility functions with enhanced scene-aware signatures +export const getCachedMedia = (url: string, sceneId?: string, projectId?: string): string | null => { + return mediaCache.get(url, sceneId, projectId); +}; + +export const setCachedMedia = ( + url: string, + blobUrl: string, + mediaType: 'image' | 'video' | 'audio', + size?: number, + sceneId?: string, + projectId?: string +): void => { + mediaCache.set(url, blobUrl, mediaType, size, sceneId, projectId); +}; + +export const hasCachedMedia = (url: string, sceneId?: string, projectId?: string): boolean => { + return mediaCache.has(url, sceneId, projectId); +}; + +export const invalidateMediaCache = (url: string, sceneId?: string, projectId?: string): void => { + mediaCache.invalidate(url, sceneId, projectId); +}; + +export const clearMediaCache = (): void => { + mediaCache.clear(); +}; + +export const clearSceneMediaCache = (sceneId: string): void => { + mediaCache.clearScene(sceneId); +}; + +export const clearProjectMediaCache = (projectId: string): void => { + mediaCache.clearProject(projectId); +}; + +export const getMediaCacheStats = () => { + return mediaCache.getStats(); +}; diff --git a/migrate_usage_summaries.py b/migrate_usage_summaries.py new file mode 100644 index 00000000..4b468b81 --- /dev/null +++ b/migrate_usage_summaries.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +Migration script to add missing wavespeed columns to usage_summaries table +""" + +import sqlite3 +import sys +import os +from pathlib import Path + +def get_db_path(): + """Find the database file""" + # Look for common database locations + possible_paths = [ + "backend/database.db", + "backend/data/database.db", + "database.db", + "data/database.db" + ] + + for path in possible_paths: + if os.path.exists(path): + return path + + # If not found, check if there's a .db file in backend directory + backend_dir = Path("backend") + if backend_dir.exists(): + for db_file in backend_dir.glob("*.db"): + return str(db_file) + + return None + +def migrate_usage_summaries(): + """Add missing wavespeed columns to usage_summaries table""" + + db_path = get_db_path() + if not db_path: + print("❌ Database file not found!") + print("Looked in:") + for path in ["backend/database.db", "backend/data/database.db", "database.db", "data/database.db"]: + print(f" - {path}") + return False + + print(f"📁 Using database: {db_path}") + + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Check if table exists + cursor.execute(""" + SELECT name FROM sqlite_master + WHERE type='table' AND name='usage_summaries' + """) + + if not cursor.fetchone(): + print("❌ Table 'usage_summaries' does not exist!") + return False + + # Get current columns + cursor.execute("PRAGMA table_info(usage_summaries)") + columns = [row[1] for row in cursor.fetchall()] + print(f"📋 Current columns: {columns}") + + # Columns to add + columns_to_add = [ + ("wavespeed_calls", "INTEGER DEFAULT 0"), + ("tavily_calls", "INTEGER DEFAULT 0"), + ("serper_calls", "INTEGER DEFAULT 0"), + ("metaphor_calls", "INTEGER DEFAULT 0"), + ("firecrawl_calls", "INTEGER DEFAULT 0"), + ("stability_calls", "INTEGER DEFAULT 0"), + ("exa_calls", "INTEGER DEFAULT 0"), + ("video_calls", "INTEGER DEFAULT 0"), + ("image_edit_calls", "INTEGER DEFAULT 0"), + ("audio_calls", "INTEGER DEFAULT 0"), + ("wavespeed_tokens", "INTEGER DEFAULT 0"), + ("wavespeed_cost", "FLOAT DEFAULT 0.0"), + ("tavily_cost", "FLOAT DEFAULT 0.0"), + ("serper_cost", "FLOAT DEFAULT 0.0"), + ("metaphor_cost", "FLOAT DEFAULT 0.0"), + ("firecrawl_cost", "FLOAT DEFAULT 0.0"), + ("stability_cost", "FLOAT DEFAULT 0.0"), + ("exa_cost", "FLOAT DEFAULT 0.0"), + ("video_cost", "FLOAT DEFAULT 0.0"), + ("image_edit_cost", "FLOAT DEFAULT 0.0"), + ("audio_cost", "FLOAT DEFAULT 0.0") + ] + + # Add missing columns + added_columns = [] + for column_name, column_def in columns_to_add: + if column_name not in columns: + print(f"➕ Adding column: {column_name}") + cursor.execute(f"ALTER TABLE usage_summaries ADD COLUMN {column_name} {column_def}") + added_columns.append(column_name) + else: + print(f"✅ Column already exists: {column_name}") + + if added_columns: + conn.commit() + print(f"🎉 Successfully added {len(added_columns)} columns:") + for col in added_columns: + print(f" - {col}") + else: + print("✅ All columns already exist!") + + # Verify the changes + cursor.execute("PRAGMA table_info(usage_summaries)") + new_columns = [row[1] for row in cursor.fetchall()] + print(f"📋 Updated columns: {new_columns}") + + conn.close() + return True + + except sqlite3.Error as e: + print(f"❌ Database error: {e}") + return False + except Exception as e: + print(f"❌ Unexpected error: {e}") + return False + +if __name__ == "__main__": + print("🚀 Starting usage_summaries migration...") + + if migrate_usage_summaries(): + print("✅ Migration completed successfully!") + sys.exit(0) + else: + print("❌ Migration failed!") + sys.exit(1) diff --git a/simple_migrate.py b/simple_migrate.py new file mode 100644 index 00000000..13ca6d38 --- /dev/null +++ b/simple_migrate.py @@ -0,0 +1,50 @@ +import sqlite3 +import os + +# Find database +db_paths = ["backend/database.db", "backend/data/database.db", "database.db", "data/database.db"] +db_path = None + +for path in db_paths: + if os.path.exists(path): + db_path = path + break + +if not db_path: + print("Database not found!") + exit(1) + +print(f"Using database: {db_path}") + +try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Check table + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='usage_summaries'") + if not cursor.fetchone(): + print("Table usage_summaries not found!") + exit(1) + + # Get columns + cursor.execute("PRAGMA table_info(usage_summaries)") + columns = [row[1] for row in cursor.fetchall()] + print(f"Columns: {columns}") + + # Check for wavespeed_calls + if "wavespeed_calls" in columns: + print("✅ wavespeed_calls column exists") + else: + print("❌ wavespeed_calls column missing") + + # Add the column + print("Adding wavespeed_calls column...") + cursor.execute("ALTER TABLE usage_summaries ADD COLUMN wavespeed_calls INTEGER DEFAULT 0") + conn.commit() + print("✅ wavespeed_calls column added") + + conn.close() + print("Migration completed!") + +except Exception as e: + print(f"Error: {e}")