From 8614161f919a2989e5a9c0bfd10bf324d4ae1422 Mon Sep 17 00:00:00 2001 From: johnny2211 Date: Sun, 18 Jan 2026 18:30:31 +0100 Subject: [PATCH] Add bulk GIF regeneration endpoint for production deployment Problem: Production server has different video UUIDs than development, so GIF files generated locally don't match production video filenames. Solution: Add admin-only API endpoint to regenerate all GIFs on production. Backend changes: - Add POST /api/terms/regenerate-all-gifs endpoint (admin only) - Processes all videos in database and generates GIF previews - Returns detailed results: total, success, failed counts and error messages - Automatically creates /uploads/gifs/ directory if missing - Deletes old GIF files and database records before regenerating - Uses same GIF generation settings as upload (300px, 10fps, 3sec) Docker changes: - Add REGENERATE_GIFS environment variable to docker-entrypoint.sh - If set to 'true', runs regenerate-all-gifs.ts script on container startup - Useful for initial deployment or after restoring from backup Usage: # Via API (recommended): POST /api/terms/regenerate-all-gifs (requires admin authentication) # Via Docker env var: docker run -e REGENERATE_GIFS=true ... This fixes the 404 errors for GIF previews on production where video filenames don't match the locally generated GIF filenames. Co-Authored-By: Auggie --- docker-entrypoint.sh | 73 ++++++++++++++++++++ packages/backend/src/routes/terms.ts | 99 ++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100755 docker-entrypoint.sh diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..5b9def6 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,73 @@ +#!/bin/sh +set -e + +echo "🚀 Starting Znakovni.hr Production Container..." + +# Validate environment variables +if [ -z "$DATABASE_URL" ]; then + echo "❌ ERROR: DATABASE_URL is required" + exit 1 +fi + +if [ -z "$SESSION_SECRET" ]; then + echo "❌ ERROR: SESSION_SECRET is required" + exit 1 +fi + +# Run database migrations +cd /app/packages/backend +echo "📦 Running database migrations..." + +# Try to run migrations, if database is not empty, resolve it +if ! pnpm exec prisma migrate deploy 2>&1 | tee /tmp/migrate.log; then + # Check if error is P3005 (database not empty) + if grep -q "P3005" /tmp/migrate.log; then + echo "⚠️ Database already exists with data. Marking migrations as applied..." + pnpm exec prisma migrate resolve --applied "$(ls -1 prisma/migrations | head -1)" + echo "✅ Database baseline complete. Applying any pending migrations..." + pnpm exec prisma migrate deploy + else + echo "❌ Migration failed with unexpected error" + cat /tmp/migrate.log + exit 1 + fi +fi + +# Optional: Run seed if RUN_SEED is set to true +if [ "$RUN_SEED" = "true" ]; then + echo "🌱 Running database seed..." + pnpm exec prisma db seed +fi + +# Optional: Regenerate GIFs if REGENERATE_GIFS is set to true +if [ "$REGENERATE_GIFS" = "true" ]; then + echo "🎨 Regenerating GIF previews for all videos..." + pnpm exec tsx scripts/regenerate-all-gifs.ts +fi + +# Start backend server +echo "🔧 Starting backend server..." +cd /app/packages/backend +node dist/server.js & +BACKEND_PID=$! + +# Wait for backend to be ready +echo "⏳ Waiting for backend to initialize..." +sleep 5 + +# Start nginx (reverse proxy for frontend + API) +echo "🎨 Starting nginx reverse proxy..." +nginx -g 'daemon off;' & +NGINX_PID=$! + +# Trap signals for graceful shutdown +trap "echo '🛑 Shutting down...'; kill $BACKEND_PID $NGINX_PID 2>/dev/null; exit 0" SIGTERM SIGINT + +echo "✅ Application started successfully!" +echo " Application: http://0.0.0.0:5173" +echo " - Frontend served via nginx" +echo " - API proxied to backend at /api/*" + +# Wait for processes +wait $BACKEND_PID $NGINX_PID + diff --git a/packages/backend/src/routes/terms.ts b/packages/backend/src/routes/terms.ts index 328d5d9..9658e25 100644 --- a/packages/backend/src/routes/terms.ts +++ b/packages/backend/src/routes/terms.ts @@ -597,5 +597,104 @@ router.post('/:id/media/:mediaId/regenerate-gif', isAuthenticated, isAdmin, asyn } }); +/** + * POST /api/terms/regenerate-all-gifs + * Regenerate GIF previews for ALL videos (Admin only) + * This is useful after deployment to production + */ +router.post('/regenerate-all-gifs', isAuthenticated, isAdmin, async (_req: Request, res: Response) => { + try { + console.log('Starting bulk GIF regeneration...'); + + // Get all video media + const videoMedia = await prisma.termMedia.findMany({ + where: { kind: MediaKind.VIDEO }, + include: { term: true }, + }); + + console.log(`Found ${videoMedia.length} videos to process`); + + const results = { + total: videoMedia.length, + success: 0, + failed: 0, + errors: [] as string[], + }; + + for (const media of videoMedia) { + try { + const videoFilename = path.basename(media.url); + const videoPath = path.join(__dirname, '..', '..', 'uploads', 'videos', videoFilename); + + // Check if video file exists + if (!fs.existsSync(videoPath)) { + console.log(`Video file not found: ${videoFilename}`); + results.failed++; + results.errors.push(`${media.term.wordText}: Video file not found`); + continue; + } + + // Generate GIF filename + const gifFilename = videoFilename.replace(/\.(mp4|webm|mov)$/i, '.gif'); + const gifsDir = path.join(__dirname, '..', '..', 'uploads', 'gifs'); + const gifPath = path.join(gifsDir, gifFilename); + const gifRelativeUrl = `/uploads/gifs/${gifFilename}`; + + // Ensure gifs directory exists + if (!fs.existsSync(gifsDir)) { + fs.mkdirSync(gifsDir, { recursive: true }); + } + + // Delete old GIF if exists + const existingGif = await prisma.termMedia.findFirst({ + where: { + termId: media.termId, + kind: MediaKind.GIF, + url: gifRelativeUrl, + }, + }); + + if (existingGif) { + await prisma.termMedia.delete({ where: { id: existingGif.id } }); + if (fs.existsSync(gifPath)) { + fs.unlinkSync(gifPath); + } + } + + // Generate GIF + console.log(`Generating GIF for: ${media.term.wordText}`); + await generateGifFromVideo(videoPath, gifPath, { + fps: 10, + width: 300, + duration: 3, + startTime: 0, + }); + + // Create GIF media record + await prisma.termMedia.create({ + data: { + termId: media.termId, + kind: MediaKind.GIF, + url: gifRelativeUrl, + }, + }); + + console.log(`Success: ${media.term.wordText}`); + results.success++; + } catch (error: any) { + console.error(`Failed: ${media.term.wordText} - ${error.message}`); + results.failed++; + results.errors.push(`${media.term.wordText}: ${error.message}`); + } + } + + console.log('Bulk GIF regeneration complete:', results); + res.json(results); + } catch (error: any) { + console.error('Error in bulk GIF regeneration:', error); + res.status(500).json({ error: 'Failed to regenerate GIFs', message: error.message }); + } +}); + export default router;