Add admin tool for bulk GIF regeneration via web UI
Created a simple HTML page that allows admins to regenerate all GIF previews directly from the browser without needing terminal access. Features: - Clean, user-friendly interface with warnings and instructions - Calls POST /api/terms/regenerate-all-gifs endpoint - Shows progress spinner during processing - Displays detailed results: total, success, failed counts - Shows error messages if any videos failed to process - Requires admin authentication (uses session cookies) - Responsive design with proper error handling Usage: 1. Deploy the new Docker image to production 2. Login as admin on the website 3. Navigate to: https://znakovni.matijaturk.from.hr/regenerate-gifs.html 4. Click 'Start GIF Regeneration' button 5. Wait for completion (may take several minutes) 6. Refresh dictionary page to see GIF previews This solves the production deployment issue where GIF files generated locally don't match production video filenames (different UUIDs). Co-Authored-By: Auggie
This commit is contained in:
191
packages/frontend/public/regenerate-gifs.html
Normal file
191
packages/frontend/public/regenerate-gifs.html
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="hr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Regenerate GIFs - Znakovni.hr Admin</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
border: 1px solid #0c5460;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
#result {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#result.success {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #28a745;
|
||||||
|
color: #155724;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#result.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #dc3545;
|
||||||
|
color: #721c24;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #007bff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🎨 Regenerate GIF Previews</h1>
|
||||||
|
<p>This tool will regenerate GIF preview images for all videos in the database.</p>
|
||||||
|
|
||||||
|
<div class="warning">
|
||||||
|
<strong>⚠️ Warning:</strong> This process may take several minutes depending on the number of videos.
|
||||||
|
Make sure you are logged in as an admin before clicking the button.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<strong>ℹ️ What this does:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Finds all videos in the database</li>
|
||||||
|
<li>Generates a 300px GIF preview (10fps, 3 seconds) for each video</li>
|
||||||
|
<li>Saves GIFs to /uploads/gifs/ directory</li>
|
||||||
|
<li>Creates database records for each GIF</li>
|
||||||
|
<li>Deletes old GIF files if they exist</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="regenerateBtn" onclick="regenerateGifs()">
|
||||||
|
🔄 Start GIF Regeneration
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="progress" class="progress">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p style="text-align: center;">Processing videos... Please wait.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function regenerateGifs() {
|
||||||
|
const btn = document.getElementById('regenerateBtn');
|
||||||
|
const progress = document.getElementById('progress');
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
|
btn.disabled = true;
|
||||||
|
progress.style.display = 'block';
|
||||||
|
result.style.display = 'none';
|
||||||
|
result.className = '';
|
||||||
|
result.innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/terms/regenerate-all-gifs', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include', // Include cookies for authentication
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
progress.style.display = 'none';
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
result.className = 'success';
|
||||||
|
result.innerHTML = `
|
||||||
|
<h3>✅ Success!</h3>
|
||||||
|
<p><strong>Total videos:</strong> ${data.total}</p>
|
||||||
|
<p><strong>Successfully processed:</strong> ${data.success}</p>
|
||||||
|
<p><strong>Failed:</strong> ${data.failed}</p>
|
||||||
|
${data.errors && data.errors.length > 0 ? `
|
||||||
|
<details>
|
||||||
|
<summary>Show errors (${data.errors.length})</summary>
|
||||||
|
<pre>${data.errors.join('\n')}</pre>
|
||||||
|
</details>
|
||||||
|
` : ''}
|
||||||
|
<p style="margin-top: 15px;">You can now refresh the dictionary page to see the GIF previews!</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || data.error || 'Unknown error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
progress.style.display = 'none';
|
||||||
|
result.className = 'error';
|
||||||
|
result.innerHTML = `
|
||||||
|
<h3>❌ Error</h3>
|
||||||
|
<p>${error.message}</p>
|
||||||
|
<p>Make sure you are logged in as an admin and try again.</p>
|
||||||
|
`;
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in New Issue
Block a user