// State let currentUser = null; let mfaTypes = []; let base64Image = ''; let currentPage = 1; let adminCurrentPage = 1; // DOM Elements const pages = { login: document.getElementById('login-page'), mfa: document.getElementById('mfa-page'), main: document.getElementById('main-page'), }; const sections = { generate: document.getElementById('generate-section'), gallery: document.getElementById('gallery-section'), admin: document.getElementById('admin-section'), }; // API Helper async function api(path, options = {}) { const response = await fetch(`/api${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Request failed'); } return data; } // Page Navigation function showPage(pageName) { Object.values(pages).forEach(p => p.classList.add('hidden')); pages[pageName]?.classList.remove('hidden'); } function showSection(sectionName) { Object.values(sections).forEach(s => s.classList.add('hidden')); sections[sectionName]?.classList.remove('hidden'); document.querySelectorAll('.nav-link').forEach(link => { link.classList.toggle('active', link.dataset.page === sectionName); }); if (sectionName === 'gallery') loadGallery(); if (sectionName === 'admin') loadUsers(); } // Auth async function checkAuth() { try { const data = await api('/auth/me'); currentUser = data.user; showMainApp(); } catch { showPage('login'); } } function showMainApp() { showPage('main'); document.getElementById('current-user').textContent = currentUser.username; if (currentUser.isAdmin) { document.querySelectorAll('.admin-only').forEach(el => el.classList.remove('hidden')); } showSection('generate'); checkPendingJobs(); } // Pending Jobs async function checkPendingJobs() { try { const data = await api('/generate/pending'); if (data.jobs && data.jobs.length > 0) { showPendingJobsNotification(data.jobs); } } catch (error) { console.error('Failed to check pending jobs:', error); } } function showPendingJobsNotification(jobs) { // Remove existing banner if any const existingBanner = document.querySelector('.pending-jobs-banner'); if (existingBanner) existingBanner.remove(); const banner = document.createElement('div'); banner.className = 'pending-jobs-banner'; banner.innerHTML = ` You have ${jobs.length} video${jobs.length > 1 ? 's' : ''} generating in the background
`; // Attach event listeners properly instead of inline onclick banner.querySelector('.view-progress-btn').addEventListener('click', function() { const contentId = parseInt(this.dataset.contentId, 10); const jobId = this.dataset.jobId; resumeLatestJob(contentId, jobId); }); banner.querySelector('.dismiss-btn').addEventListener('click', function() { banner.remove(); }); document.querySelector('.main-content').prepend(banner); } async function resumeLatestJob(contentId, jobId) { const statusEl = document.getElementById('generation-status'); const videoEl = document.getElementById('output-video'); const btn = document.getElementById('generate-btn'); try { // Switch to generate tab and show progress showSection('generate'); btn.disabled = true; btn.textContent = 'Generating...'; statusEl.className = 'status-message info'; statusEl.textContent = 'Resuming job...'; statusEl.classList.remove('hidden'); videoEl.classList.add('hidden'); // Remove the banner const banner = document.querySelector('.pending-jobs-banner'); if (banner) banner.remove(); // Poll for completion await pollJob(jobId, contentId, statusEl, videoEl); } catch (error) { console.error('Error resuming job:', error); statusEl.className = 'status-message error'; statusEl.textContent = error.message || 'Failed to resume job'; statusEl.classList.remove('hidden'); } finally { btn.disabled = false; btn.textContent = 'Generate Video'; } } // Login document.getElementById('login-form').addEventListener('submit', async (e) => { e.preventDefault(); const errorEl = document.getElementById('login-error'); errorEl.textContent = ''; const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const data = await api('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }), }); if (data.requiresMfa) { mfaTypes = data.mfaTypes; showMfaPage(); } else { currentUser = data.user; showMainApp(); } } catch (error) { errorEl.textContent = error.message; } }); function showMfaPage() { showPage('mfa'); const totpSection = document.getElementById('totp-section'); const webauthnSection = document.getElementById('webauthn-section'); totpSection.classList.toggle('hidden', !mfaTypes.includes('totp')); webauthnSection.classList.toggle('hidden', !mfaTypes.includes('webauthn')); } // TOTP Verification document.getElementById('totp-form').addEventListener('submit', async (e) => { e.preventDefault(); const errorEl = document.getElementById('mfa-error'); errorEl.textContent = ''; const code = document.getElementById('totp-code').value; try { const data = await api('/auth/mfa/totp', { method: 'POST', body: JSON.stringify({ code }), }); currentUser = data.user; showMainApp(); } catch (error) { errorEl.textContent = error.message; } }); // WebAuthn document.getElementById('webauthn-btn').addEventListener('click', async () => { const errorEl = document.getElementById('mfa-error'); errorEl.textContent = ''; try { const options = await api('/auth/mfa/webauthn/challenge', { method: 'POST' }); const credential = await navigator.credentials.get({ publicKey: { ...options, challenge: base64UrlToBuffer(options.challenge), allowCredentials: options.allowCredentials?.map(c => ({ ...c, id: base64UrlToBuffer(c.id), })), }, }); const response = { id: credential.id, rawId: bufferToBase64Url(credential.rawId), type: credential.type, response: { clientDataJSON: bufferToBase64Url(credential.response.clientDataJSON), authenticatorData: bufferToBase64Url(credential.response.authenticatorData), signature: bufferToBase64Url(credential.response.signature), }, }; const data = await api('/auth/mfa/webauthn/verify', { method: 'POST', body: JSON.stringify(response), }); currentUser = data.user; showMainApp(); } catch (error) { errorEl.textContent = error.message || 'WebAuthn verification failed'; } }); // Back to login from MFA document.getElementById('mfa-back').addEventListener('click', async () => { await api('/auth/logout', { method: 'POST' }); showPage('login'); }); // Logout document.getElementById('logout-btn').addEventListener('click', async () => { await api('/auth/logout', { method: 'POST' }); currentUser = null; showPage('login'); }); // Navigation document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); showSection(link.dataset.page); }); }); // Image Upload const imageInput = document.getElementById('image-input'); const imageUploadArea = document.getElementById('image-upload-area'); const previewImage = document.getElementById('preview-image'); imageInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { previewImage.src = event.target.result; previewImage.classList.remove('hidden'); imageUploadArea.classList.add('has-file'); base64Image = event.target.result.split(',')[1]; }; reader.readAsDataURL(file); } }); // Generation document.getElementById('generate-form').addEventListener('submit', async (e) => { e.preventDefault(); if (!base64Image) { showStatus('Please upload an image first', 'error'); return; } const btn = document.getElementById('generate-btn'); btn.disabled = true; btn.textContent = 'Generating...'; const statusEl = document.getElementById('generation-status'); const videoEl = document.getElementById('output-video'); statusEl.className = 'status-message info'; statusEl.textContent = 'Submitting job...'; statusEl.classList.remove('hidden'); videoEl.classList.add('hidden'); try { const formData = { image: base64Image, prompt: document.getElementById('prompt').value, negativePrompt: document.getElementById('negative-prompt').value, resolution: parseInt(document.getElementById('resolution').value), steps: parseInt(document.getElementById('steps').value), }; const submitData = await api('/generate', { method: 'POST', body: JSON.stringify(formData), }); const { jobId, contentId } = submitData; statusEl.textContent = `Job submitted. ID: ${jobId}. Waiting for completion...`; // Poll for completion await pollJob(jobId, contentId, statusEl, videoEl); } catch (error) { showStatus(error.message, 'error'); } finally { btn.disabled = false; btn.textContent = 'Generate Video'; } }); async function pollJob(jobId, contentId, statusEl, videoEl) { const startTime = Date.now(); const maxTime = 25 * 60 * 1000; // 25 minutes while (Date.now() - startTime < maxTime) { const elapsed = Math.floor((Date.now() - startTime) / 1000); statusEl.textContent = `Generating... (${elapsed}s elapsed)`; try { const status = await api(`/generate/${jobId}/status`); if (status.status === 'COMPLETED') { statusEl.className = 'status-message success'; statusEl.textContent = 'Generation complete!'; // Load video videoEl.src = `/api/content/${contentId}/stream`; videoEl.classList.remove('hidden'); videoEl.play(); return; } if (status.status === 'FAILED') { throw new Error(status.error || 'Generation failed'); } await new Promise(r => setTimeout(r, 5000)); } catch (error) { showStatus(error.message, 'error'); return; } } showStatus('Generation timed out', 'error'); } function showStatus(message, type) { const statusEl = document.getElementById('generation-status'); statusEl.className = `status-message ${type}`; statusEl.textContent = message; statusEl.classList.remove('hidden'); } // Gallery async function loadGallery(page = 1) { currentPage = page; const grid = document.getElementById('gallery-grid'); const pagination = document.getElementById('gallery-pagination'); const status = document.getElementById('status-filter').value; grid.innerHTML = '
'; try { const params = new URLSearchParams({ page, limit: 12 }); if (status) params.append('status', status); const data = await api(`/content?${params}`); renderGallery(grid, data.content); renderPagination(pagination, data.pagination, loadGallery); } catch (error) { grid.innerHTML = `

${error.message}

`; } } document.getElementById('status-filter').addEventListener('change', () => loadGallery(1)); function renderGallery(container, items) { if (items.length === 0) { container.innerHTML = '

No content found

'; return; } container.innerHTML = items.map(item => ` `).join(''); } async function deleteContent(id) { if (!confirm('Are you sure you want to delete this content?')) return; try { await api(`/content/${id}`, { method: 'DELETE' }); loadGallery(currentPage); } catch (error) { alert(error.message); } } // Admin document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.admin-tab').forEach(t => t.classList.add('hidden')); btn.classList.add('active'); document.getElementById(`${btn.dataset.tab}-tab`).classList.remove('hidden'); if (btn.dataset.tab === 'all-content') loadAdminGallery(); }); }); async function loadUsers() { const container = document.getElementById('users-list'); container.innerHTML = '
'; try { const data = await api('/users'); container.innerHTML = data.users.map(user => `

${escapeHtml(user.username)}

${escapeHtml(user.email || 'No email')}

${user.isAdmin ? 'Admin' : ''} ${!user.isActive ? 'Inactive' : ''}
${user.id !== currentUser.id ? `` : ''}
`).join(''); } catch (error) { container.innerHTML = `

${error.message}

`; } } document.getElementById('add-user-btn').addEventListener('click', () => { showModal(`
`); document.getElementById('add-user-form').addEventListener('submit', async (e) => { e.preventDefault(); const form = e.target; try { await api('/users', { method: 'POST', body: JSON.stringify({ username: form.username.value, password: form.password.value, email: form.email.value || null, isAdmin: form.isAdmin.checked, }), }); hideModal(); loadUsers(); } catch (error) { alert(error.message); } }); }); async function editUser(id) { try { const data = await api(`/users/${id}`); const user = data.user; showModal(`

`); document.getElementById('edit-user-form').addEventListener('submit', async (e) => { e.preventDefault(); const form = e.target; try { await api(`/users/${id}`, { method: 'PUT', body: JSON.stringify({ username: form.username.value, email: form.email.value || null, isAdmin: form.isAdmin.checked, isActive: form.isActive.checked, }), }); if (form.newPassword.value) { await api(`/users/${id}/reset-password`, { method: 'POST', body: JSON.stringify({ newPassword: form.newPassword.value }), }); } hideModal(); loadUsers(); } catch (error) { alert(error.message); } }); } catch (error) { alert(error.message); } } async function deleteUser(id) { if (!confirm('Are you sure you want to delete this user?')) return; try { await api(`/users/${id}`, { method: 'DELETE' }); loadUsers(); } catch (error) { alert(error.message); } } async function loadAdminGallery(page = 1) { adminCurrentPage = page; const grid = document.getElementById('admin-gallery-grid'); const pagination = document.getElementById('admin-gallery-pagination'); grid.innerHTML = '
'; try { const data = await api(`/content?page=${page}&limit=12`); renderGallery(grid, data.content); renderPagination(pagination, data.pagination, loadAdminGallery); } catch (error) { grid.innerHTML = `

${error.message}

`; } } // Modal function showModal(content) { document.getElementById('modal-content').innerHTML = content; document.getElementById('modal-overlay').classList.remove('hidden'); } function hideModal() { document.getElementById('modal-overlay').classList.add('hidden'); } document.getElementById('modal-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) hideModal(); }); // Pagination function renderPagination(container, pagination, loadFn) { const { page, totalPages } = pagination; if (totalPages <= 1) { container.innerHTML = ''; return; } let html = ''; html += ``; for (let i = 1; i <= totalPages; i++) { if (i === 1 || i === totalPages || (i >= page - 1 && i <= page + 1)) { html += ``; } else if (i === page - 2 || i === page + 2) { html += '...'; } } html += ``; container.innerHTML = html; } // Utilities function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function formatDate(dateStr) { return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function base64UrlToBuffer(base64url) { const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/'); const padding = '='.repeat((4 - base64.length % 4) % 4); const binary = atob(base64 + padding); return Uint8Array.from(binary, c => c.charCodeAt(0)).buffer; } function bufferToBase64Url(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // Init checkAuth();