// WebSocket and application logic for BDFR Web Interface class BDFRApp { constructor() { this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; this.downloads = new Map(); this.authState = null; this.authenticated = false; this.initializeElements(); this.bindEvents(); this.connectWebSocket(); this.updateStatus(); this.startStatusPolling(); // Check for stored auth state first this.authState = this.getStoredAuthState(); this.checkAuthentication(); } initializeElements() { // Forms this.unifiedForm = document.getElementById('unifiedForm'); this.subredditForm = document.getElementById('subredditForm'); this.userForm = document.getElementById('userForm'); // Progress containers this.progressContainer = document.getElementById('progressContainer'); this.downloadsList = document.getElementById('downloadsList'); this.downloadsItems = document.getElementById('downloadsItems'); // Status elements this.wsStatus = document.getElementById('wsStatus'); this.bdfrStatus = document.getElementById('bdfrStatus'); // Authentication elements this.authSection = document.getElementById('authSection'); this.authUser = document.getElementById('authUser'); this.authStatus = document.getElementById('authStatus'); this.loginBtn = document.getElementById('loginBtn'); this.logoutBtn = document.getElementById('logoutBtn'); this.authStateInput = document.getElementById('authState'); this.userAuthStateInput = document.getElementById('userAuthState'); } bindEvents() { // Form submissions if (this.unifiedForm) { this.unifiedForm.addEventListener('submit', (e) => this.handleUnifiedSubmit(e)); // Source type toggle const sourceTypeRadios = this.unifiedForm.querySelectorAll('input[name="source_type"]'); sourceTypeRadios.forEach(radio => { radio.addEventListener('change', (e) => this.updateSourceTypeUI(e.target.value)); }); } if (this.subredditForm) { this.subredditForm.addEventListener('submit', (e) => this.handleSubredditSubmit(e)); } if (this.userForm) { this.userForm.addEventListener('submit', (e) => this.handleUserSubmit(e)); } // Authentication events if (this.loginBtn) { this.loginBtn.addEventListener('click', () => this.handleLogin()); } if (this.logoutBtn) { this.logoutBtn.addEventListener('click', () => this.handleLogout()); } // Real-time input validation ['subreddit', 'username', 'sourceName'].forEach(id => { const input = document.getElementById(id); if (input) { input.addEventListener('input', (e) => this.validateInput(e.target)); } }); } async connectWebSocket() { try { // Force HTTP for development - change this for production const protocol = 'ws:'; const host = window.location.hostname || 'localhost'; const port = window.location.port || '8000'; const wsUrl = `${protocol}//${host}:${port}/ws/progress`; console.log('[FRONTEND-WS] Attempting to connect to WebSocket:', wsUrl); console.log('[FRONTEND-WS] Window location:', window.location.href); console.log('[FRONTEND-WS] Host:', window.location.host); console.log('[FRONTEND-WS] Hostname:', window.location.hostname); console.log('[FRONTEND-WS] Port:', window.location.port); this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('[FRONTEND-WS] WebSocket connected successfully'); this.reconnectAttempts = 0; this.updateWSStatus('Connected'); }; this.ws.onmessage = (event) => { console.log('[FRONTEND-WS] Received message:', event.data); const data = JSON.parse(event.data); this.handleWebSocketMessage(data); }; this.ws.onclose = (event) => { console.log('[FRONTEND-WS] WebSocket disconnected:', event.code, event.reason); this.updateWSStatus('Disconnected'); this.scheduleReconnect(); }; this.ws.onerror = (error) => { console.error('[FRONTEND-WS] WebSocket error:', error); this.updateWSStatus('Error'); }; } catch (error) { console.error('Failed to connect WebSocket:', error); this.scheduleReconnect(); } } scheduleReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); console.log(`[FRONTEND-WS] Scheduling reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`); setTimeout(() => { console.log(`[FRONTEND-WS] Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); this.connectWebSocket(); }, delay); } else { console.error('[FRONTEND-WS] Failed to connect after maximum attempts'); this.updateWSStatus('Failed to connect'); } } handleWebSocketMessage(data) { console.log('WebSocket message received:', data); // Ensure data has the required fields if (!data.download_id && data.id) { data.download_id = data.id; } switch (data.type) { case 'progress': case 'status_update': this.updateProgress(data); break; case 'status': this.showNotification(`Download ${data.download_id}: ${data.message}`, 'info'); break; case 'completed': this.showSuccess(`Download ${data.download_id} completed!`); this.updateProgress(data); // Also update progress for completion break; case 'error': this.showError(`Download ${data.download_id} failed: ${data.message}`); this.updateProgress(data); // Also update progress for errors break; default: console.log('Unknown message type:', data.type, data); } } async handleSubredditSubmit(e) { e.preventDefault(); const formData = new FormData(e.target); const subreddit = formData.get('subreddit').trim(); if (!this.validateSubreddit(subreddit)) { this.showError('Please enter a valid subreddit name'); return; } // Get no_dupes checkbox value and add it to formData const noDupes = document.getElementById('noDupes').checked; formData.set('no_dupes', noDupes ? 'true' : 'false'); // Show additional options for confirmation const limit = formData.get('limit'); const sort = formData.get('sort'); if (confirm(`Start download for r/${subreddit}?\n\nOptions:\n- Limit: ${limit}\n- Sort: ${sort}\n- No duplicates: ${noDupes ? 'Yes' : 'No'}`)) { try { this.showLoading(e.target.querySelector('button')); const response = await fetch('/api/download/subreddit', { method: 'POST', body: formData }); const result = await response.json(); if (response.ok) { this.showSuccess(`Subreddit download started! ID: ${result.download_id}`); e.target.reset(); } else { this.showError(result.detail || 'Failed to start download'); } } catch (error) { console.error('Error:', error); this.showError('Network error occurred'); } finally { this.hideLoading(e.target.querySelector('button')); } } } async handleUserSubmit(e) { e.preventDefault(); const formData = new FormData(e.target); const username = formData.get('username').trim(); if (!this.validateUsername(username)) { this.showError('Please enter a valid username'); return; } // Get no_dupes checkbox value and add it to formData const noDupes = document.getElementById('userNoDupes').checked; formData.set('no_dupes', noDupes ? 'true' : 'false'); // Get content type for confirmation const contentType = formData.get('content_type'); const contentTypeLabel = contentType === 'submitted' ? 'submitted posts' : contentType === 'upvoted' ? 'upvoted posts' : 'saved posts'; if (confirm(`Start download for u/${username}?\n\nContent: ${contentTypeLabel}\nLimit: ${formData.get('limit')}`)) { try { this.showLoading(e.target.querySelector('button')); const response = await fetch('/api/download/user', { method: 'POST', body: formData }); const result = await response.json(); if (response.ok) { this.showSuccess(`User download started! ID: ${result.download_id}`); e.target.reset(); } else { this.showError(result.detail || 'Failed to start download'); } } catch (error) { console.error('Error:', error); this.showError('Network error occurred'); } finally { this.hideLoading(e.target.querySelector('button')); } } } updateSourceTypeUI(sourceType) { const sourceNameLabel = document.getElementById('sourceNameLabel'); const sourceNameInput = document.getElementById('sourceName'); const sourceNameHelp = document.getElementById('sourceNameHelp'); if (sourceType === 'subreddit') { sourceNameLabel.textContent = 'Subreddit Name:'; sourceNameInput.placeholder = 'e.g., python, machinelearning'; sourceNameHelp.textContent = "Enter subreddit name without 'r/'"; } else { sourceNameLabel.textContent = 'Username:'; sourceNameInput.placeholder = 'e.g., spez, your_username'; sourceNameHelp.textContent = "Enter Reddit username without 'u/'"; } } async handleUnifiedSubmit(e) { e.preventDefault(); const formData = new FormData(e.target); const downloadMode = formData.get('download_mode'); const sourceType = formData.get('source_type'); const sourceName = formData.get('source_name').trim(); const limit = formData.get('limit'); const sort = formData.get('sort'); const noDupes = document.getElementById('noDupes').checked; const simpleCheck = document.getElementById('simpleCheck').checked; // Validate source name if (sourceType === 'subreddit' && !this.validateSubreddit(sourceName)) { this.showError('Please enter a valid subreddit name'); return; } if (sourceType === 'user' && !this.validateUsername(sourceName)) { this.showError('Please enter a valid username'); return; } // Build confirmation message const modeLabels = { 'download': 'Download (media files)', 'archive': 'Archive (metadata only)', 'clone': 'Clone (media + metadata)' }; const sourceLabel = sourceType === 'subreddit' ? `r/${sourceName}` : `u/${sourceName}`; if (confirm(`Start ${modeLabels[downloadMode]} for ${sourceLabel}?\n\nOptions:\n- Limit: ${limit}\n- Sort: ${sort}\n- No duplicates: ${noDupes ? 'Yes' : 'No'}\n- Simple check: ${simpleCheck ? 'Yes' : 'No'}`)) { try { this.showLoading(e.target.querySelector('button')); // Prepare form data for backend const backendFormData = new FormData(); if (sourceType === 'subreddit') { backendFormData.append('subreddit', sourceName); } else { backendFormData.append('username', sourceName); } backendFormData.append('limit', limit); backendFormData.append('sort', sort); backendFormData.append('time_filter', formData.get('time_filter') || ''); backendFormData.append('min_score', formData.get('min_score') || ''); backendFormData.append('no_dupes', noDupes ? 'true' : 'false'); backendFormData.append('simple_check', simpleCheck ? 'true' : 'false'); backendFormData.append('make_hard_links', formData.get('make_hard_links') ? 'true' : 'false'); backendFormData.append('download_mode', downloadMode); if (this.authState) { backendFormData.append('auth_state', this.authState); } // Determine endpoint based on source type const endpoint = sourceType === 'subreddit' ? '/api/download/subreddit' : '/api/download/user'; // For user downloads, always set submitted=true (default) if (sourceType === 'user') { backendFormData.append('submitted', 'true'); } const response = await fetch(endpoint, { method: 'POST', body: backendFormData }); const result = await response.json(); if (response.ok) { this.showSuccess(`${modeLabels[downloadMode]} started! ID: ${result.download_id}`); e.target.reset(); } else { this.showError(result.detail || 'Failed to start download'); } } catch (error) { console.error('Error:', error); this.showError('Network error occurred'); } finally { this.hideLoading(e.target.querySelector('button')); } } } updateProgress(data) { const download_id = data.download_id || data.id; console.log('Updating progress for:', download_id, data); if (!this.downloads.has(download_id)) { console.log('Creating new progress card for:', download_id); this.createProgressCard(download_id, data); } const card = this.downloads.get(download_id); if (card) { console.log('Updating existing progress card for:', download_id); this.updateProgressCard(card, data); } this.showDownloadsList(); } createProgressCard(downloadId, data) { const card = document.createElement('div'); card.className = 'progress-card'; card.id = `progress-${downloadId}`; const typeLabel = data.type === 'subreddit' ? 'Subreddit' : 'User'; const targetName = data.subreddit || data.username || 'Unknown'; const itemsProcessedInit = Number.isFinite(data.items_processed) ? data.items_processed : (data.data && Number.isFinite(data.data.items_processed) ? data.data.items_processed : 0); const limitInit = Number.isFinite(data.limit) ? data.limit : (data.data && Number.isFinite(data.data.limit) ? data.data.limit : undefined); const itemsFoundInit = Number.isFinite(data.items_found) ? data.items_found : (data.data && Number.isFinite(data.data.items_found) ? data.data.items_found : undefined); const totalInit = (limitInit && limitInit > 0) ? limitInit : itemsFoundInit; card.innerHTML = `

${typeLabel}: ${targetName}

ID: ${downloadId}
Phase: ${this.getPhaseLabel(data.phase || 'queued')}
Current: ${data.current_item_type === 'submission' ? 'Post' : 'Entry'} ${data.current_item || ''}
${data.status}
${data.progress || 0}% complete ${Number.isFinite(totalInit) ? `(${itemsProcessedInit}/${totalInit} items)` : ''} - ${data.message || 'Starting...'}
${this.getProgressDetails(data)}
`; // Persist basics for retry card.dataset.subreddit = data.subreddit || ''; card.dataset.username = data.username || ''; if (Number.isFinite(totalInit)) { card.dataset.limit = String(totalInit); } // Attach retry button immediately if already failed due to rate limit this.maybeAttachRetry(card, data, downloadId); this.downloads.set(downloadId, card); this.downloadsItems.appendChild(card); } updateProgressCard(card, data) { const statusElement = card.querySelector('.progress-status'); const progressFill = card.querySelector('.progress-fill'); const progressText = card.querySelector('.progress-text'); const phaseElement = card.querySelector('.progress-phase'); const downloadId = data.download_id || data.id; const currentItemElement = card.querySelector(`#current-item-${downloadId}`); const progressDetailsElement = card.querySelector(`#progress-details-${downloadId}`); // Update status statusElement.textContent = data.status; statusElement.className = `progress-status status-${data.status}`; // Update phase if (phaseElement) { phaseElement.textContent = `Phase: ${this.getPhaseLabel(data.phase || 'queued')}`; } // Update current item if (currentItemElement) { if (data.current_item) { const itemTypeLabel = data.current_item_type === 'submission' ? 'Post' : 'Entry'; currentItemElement.textContent = `Current: ${itemTypeLabel} ${data.current_item}`; currentItemElement.style.display = 'block'; } else { currentItemElement.style.display = 'none'; } } // Update progress bar if (progressFill) { progressFill.style.width = `${data.progress || 0}%`; } // Update progress text if (progressText) { const itemsProcessed = Number.isFinite(data.items_processed) ? data.items_processed : (data.data && Number.isFinite(data.data.items_processed) ? data.data.items_processed : 0); const limit = Number.isFinite(data.limit) ? data.limit : (data.data && Number.isFinite(data.data.limit) ? data.data.limit : undefined); const itemsFound = Number.isFinite(data.items_found) ? data.items_found : (data.data && Number.isFinite(data.data.items_found) ? data.data.items_found : undefined); const totalItems = (limit && limit > 0) ? limit : itemsFound; let progressTextContent = `${data.progress || 0}% complete`; if (Number.isFinite(totalItems)) { progressTextContent += ` (${itemsProcessed}/${totalItems} items)`; } progressTextContent += ` - ${data.message || 'Processing...'}`; progressText.textContent = progressTextContent; } // Update progress details if (progressDetailsElement) { progressDetailsElement.innerHTML = this.getProgressDetails(data); } // Update card class for animations card.className = `progress-card ${data.status}`; // Persist latest basics for retry if (data.subreddit) card.dataset.subreddit = data.subreddit; if (data.username) card.dataset.username = data.username; if (Number.isFinite(data.limit)) { card.dataset.limit = String(data.limit); } else if (data.data && Number.isFinite(data.data.limit)) { card.dataset.limit = String(data.data.limit); } // Attach retry button if applicable this.maybeAttachRetry(card, data, downloadId); } getPhaseLabel(phase) { const phaseLabels = { 'queued': 'Queued', 'fetching_submissions': 'Fetching Posts', 'preparing_download': 'Preparing Download', 'downloading_submission': 'Downloading Post', 'writing_file': 'Writing Files', 'writing_entry': 'Writing Entry', 'calculating_hashes': 'Calculating Hashes', 'completed': 'Completed', 'failed': 'Failed', 'cancelled': 'Cancelled' }; return phaseLabels[phase] || phase.charAt(0).toUpperCase() + phase.slice(1); } getProgressDetails(data) { let details = ''; if (data.current_subreddit) { details += `
Subreddit: ${data.current_subreddit}
`; } if (data.file_count) { details += `
Files to Hash: ${data.file_count}
`; } if (data.current_file) { const shortPath = data.current_file.length > 50 ? '...' + data.current_file.slice(-47) : data.current_file; details += `
Writing: ${shortPath}
`; } return details; } showDownloadsList() { if (this.downloads.size > 0) { this.progressContainer.style.display = 'none'; this.downloadsList.style.display = 'block'; } else { this.progressContainer.style.display = 'flex'; this.downloadsList.style.display = 'none'; } } validateInput(input) { const isValid = input.value.trim().length > 0; input.style.borderColor = isValid ? '#28a745' : '#dc3545'; return isValid; } validateSubreddit(subreddit) { return subreddit.length > 0 && subreddit.match(/^[a-zA-Z0-9_]+$/); } validateUsername(username) { return username.length > 0 && username.match(/^[a-zA-Z0-9_-]+$/); } showLoading(button) { if (button) { button.disabled = true; button.textContent = 'Processing...'; } } hideLoading(button) { if (button) { button.disabled = false; button.textContent = button.classList.contains('btn-primary') ? 'Start Download' : 'Start Download'; } } showSuccess(message) { this.showNotification(message, 'success'); } showError(message) { this.showNotification(message, 'error'); } showNotification(message, type) { // Create notification element const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.textContent = message; // Style the notification Object.assign(notification.style, { position: 'fixed', top: '20px', right: '20px', padding: '15px 20px', borderRadius: '8px', color: 'white', fontWeight: '600', zIndex: '1000', opacity: '0', transform: 'translateY(-20px)', transition: 'all 0.3s ease', backgroundColor: type === 'success' ? '#28a745' : '#dc3545' }); document.body.appendChild(notification); // Animate in setTimeout(() => { notification.style.opacity = '1'; notification.style.transform = 'translateY(0)'; }, 100); // Remove after 5 seconds setTimeout(() => { notification.style.opacity = '0'; notification.style.transform = 'translateY(-20px)'; setTimeout(() => { document.body.removeChild(notification); }, 300); }, 5000); } updateWSStatus(status) { if (this.wsStatus) { this.wsStatus.textContent = status; this.wsStatus.className = `status-value status-${status.toLowerCase()}`; } } async updateStatus() { // try { // // Update server time // this.serverTime.textContent = new Date().toLocaleTimeString(); // // Check BDFR status // const response = await fetch('/api/bdfr/status'); // const status = await response.json(); // if (this.bdfrStatus) { // this.bdfrStatus.textContent = status.bdfr_available ? 'Online' : 'Offline'; // this.bdfrStatus.className = `status-value status-${status.bdfr_available ? 'online' : 'offline'}`; // } // } catch (error) { // console.error('Failed to update status:', error); // } } startStatusPolling() { // Update status every 30 seconds setInterval(() => { this.updateStatus(); }, 30000); // Initial update this.updateStatus(); } async clearCompletedDownloads() { const completedCards = Array.from(this.downloadsItems.querySelectorAll('.progress-card.completed')); if (completedCards.length === 0) { this.showNotification('No completed downloads to clear', 'info'); return; } if (confirm(`Clear ${completedCards.length} completed download(s)?`)) { completedCards.forEach(card => { const downloadId = card.id.replace('progress-', ''); this.downloads.delete(downloadId); card.remove(); }); this.showDownloadsList(); this.showSuccess('Completed downloads cleared'); } } // Retry helpers isRateLimited(data) { const msg = (data && data.message) ? String(data.message) : ''; const ex = (data && data.data && data.data.exception) ? String(data.data.exception) : ''; const phase = data && data.data && data.data.phase; return phase === 'rate_limited' || msg.includes('429') || ex.includes('429'); } maybeAttachRetry(card, data, downloadId) { try { const controls = card.querySelector('.progress-controls'); if (!controls) return; const existing = controls.querySelector('.btn-retry'); // Detect current event as rate-limited and persist this state on the card const detected = this.isRateLimited(data); if (detected) { card.dataset.rateLimited = 'true'; } const persisted = card.dataset.rateLimited === 'true'; const shouldShow = data.status === 'failed' && (detected || persisted); // Remove if no longer applicable; otherwise ensure present if (!shouldShow) { if (existing) existing.remove(); return; } if (!existing) { const btn = document.createElement('button'); btn.className = 'btn-retry'; btn.textContent = 'Retry'; btn.title = 'Rate limited (429). Retry now.'; btn.onclick = () => this.retryDownload(downloadId); controls.appendChild(btn); } } catch (e) { console.warn('Failed to attach retry button', e); } } async retryDownload(downloadId) { try { const card = this.downloads.get(downloadId) || document.getElementById(`progress-${downloadId}`); if (!card) { this.showError(`Cannot retry; card not found for ${downloadId}`); return; } const subreddit = card.dataset.subreddit || ''; const username = card.dataset.username || ''; const limitStr = card.dataset.limit || ''; const limit = parseInt(limitStr, 10); const hasLimit = Number.isFinite(limit); let endpoint = ''; const formData = new FormData(); if (subreddit) { endpoint = '/api/download/subreddit'; formData.append('subreddit', subreddit); formData.append('limit', hasLimit ? String(limit) : '10'); formData.append('sort', 'hot'); // Enable no_dupes for retry to avoid re-downloading same files formData.append('no_dupes', 'true'); if (this.authState) formData.append('auth_state', this.authState); } else if (username) { endpoint = '/api/download/user'; formData.append('username', username); formData.append('limit', hasLimit ? String(limit) : '10'); formData.append('submitted', 'true'); // Enable no_dupes for retry to avoid re-downloading same files formData.append('no_dupes', 'true'); if (this.authState) formData.append('auth_state', this.authState); } else { this.showError('Cannot determine original request (subreddit/user) for retry'); return; } // Remove old failed card before retrying if (card.remove) card.remove(); this.downloads.delete(downloadId); this.showNotification(`Retrying ${subreddit ? `r/${subreddit}` : `u/${username}`}...`, 'info'); const response = await fetch(endpoint, { method: 'POST', body: formData }); const result = await response.json(); if (response.ok) { this.showSuccess(`Retry started: ${result.download_id}`); // New download card will appear via websocket updates } else { this.showError(result.detail || 'Failed to start retry'); } } catch (error) { console.error('Retry error:', error); this.showError('Retry failed'); } } // Authentication methods async checkAuthentication() { try { // Use stored auth state if available const stateToCheck = this.authState || this.getStoredAuthState(); const response = await fetch(`/api/auth/status${stateToCheck ? `?state=${stateToCheck}` : ''}`); const authData = await response.json(); console.log('Auth check:', { stateUsed: stateToCheck, authenticated: authData.authenticated, message: authData.message }); this.authenticated = authData.authenticated; // If authenticated, store the state for future use if (authData.authenticated && stateToCheck) { this.authState = stateToCheck; this.storeAuthState(stateToCheck); } this.updateAuthDisplay(authData); } catch (error) { console.error('Failed to check authentication:', error); } } getStoredAuthState() { // Try to get stored state from sessionStorage return sessionStorage.getItem('bdfr_auth_state'); } storeAuthState(state) { // Store state in sessionStorage for persistence sessionStorage.setItem('bdfr_auth_state', state); } updateAuthDisplay(authData) { if (authData.authenticated) { this.authSection.style.display = 'block'; this.authStatus.textContent = '🟢 Connected'; this.authStatus.className = 'auth-status-indicator connected'; this.loginBtn.style.display = 'none'; this.logoutBtn.style.display = 'inline-block'; // Show Reddit username when authenticated if (this.authUser) this.authUser.textContent = (authData.username || '').toString() || '-'; // Update forms with auth state if (this.authStateInput) this.authStateInput.value = this.authState || ''; if (this.userAuthStateInput) this.userAuthStateInput.value = this.authState || ''; } else { this.authSection.style.display = 'block'; this.authStatus.textContent = '🔴 Not Connected'; this.authStatus.className = 'auth-status-indicator disconnected'; this.loginBtn.style.display = 'inline-block'; this.logoutBtn.style.display = 'none'; // Reset Reddit username display if (this.authUser) this.authUser.textContent = '-'; // Clear auth state from forms if (this.authStateInput) this.authStateInput.value = ''; if (this.userAuthStateInput) this.userAuthStateInput.value = ''; } } async handleLogin() { try { // Get OAuth2 authorization URL const response = await fetch('/auth/login'); const authData = await response.json(); if (response.ok) { // Store state for later use this.authState = authData.state; this.storeAuthState(this.authState); // Redirect to Reddit OAuth2 window.location.href = authData.authorization_url; } else { this.showError(authData.detail || 'Failed to initiate login'); } } catch (error) { console.error('Login error:', error); this.showError('Failed to connect to Reddit'); } } async handleLogout() { if (!this.authState) { this.showError('No active session to logout'); return; } try { const formData = new FormData(); formData.append('state', this.authState); const response = await fetch('/auth/logout', { method: 'POST', body: formData }); if (response.ok) { this.authState = null; this.authenticated = false; // Clear stored auth state sessionStorage.removeItem('bdfr_auth_state'); this.updateAuthDisplay({ authenticated: false }); this.showSuccess('Successfully logged out'); } else { const error = await response.json(); this.showError(error.detail || 'Logout failed'); } } catch (error) { console.error('Logout error:', error); this.showError('Logout failed'); } } // Handle OAuth2 callback handleOAuth2Callback() { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); const state = urlParams.get('state'); const error = urlParams.get('error'); if (error) { this.showError(`OAuth2 error: ${error}`); return; } if (code && state) { this.completeOAuth2Flow(code, state); } } async completeOAuth2Flow(code, state) { try { const formData = new FormData(); formData.append('code', code); formData.append('state', state); const response = await fetch('/auth/callback', { method: 'POST', body: formData }); if (response.ok) { // Check if response is HTML (success page) or JSON const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { // It's the success page, just show success message this.showSuccess('Successfully authenticated with Reddit!'); this.authenticated = true; // Store auth state for persistence (use a default since we don't have it from HTML response) // The actual state will be retrieved when checking auth status this.storeAuthState('active_session'); this.updateAuthDisplay({ authenticated: true }); // Clean URL window.history.replaceState({}, document.title, window.location.pathname); } else { // It's JSON response (fallback) const result = await response.json(); this.authState = result.tokens ? result.tokens.state : null; this.authenticated = true; this.updateAuthDisplay({ authenticated: true }); // Update user display if available if (result.user && this.authUser) { this.authUser.textContent = result.user.name; } this.showSuccess('Successfully authenticated with Reddit!'); window.history.replaceState({}, document.title, window.location.pathname); } } else { // Try to parse as JSON, but handle HTML error pages gracefully try { const result = await response.json(); this.showError(result.detail || 'Authentication failed'); } catch (parseError) { // If it's not JSON, it might be an HTML error page this.showError('Authentication failed - please check your OAuth configuration'); } } } catch (error) { console.error('OAuth2 completion error:', error); this.showError('Authentication failed'); } } } // Initialize the application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.bdfrApp = new BDFRApp(); // Check for OAuth2 callback parameters const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('code') || urlParams.has('error')) { window.bdfrApp.handleOAuth2Callback(); } // Check for authentication success indicator if (urlParams.has('authenticated')) { window.bdfrApp.showSuccess('Successfully authenticated with Reddit!'); window.bdfrApp.authenticated = true; window.bdfrApp.updateAuthDisplay({ authenticated: true }); window.history.replaceState({}, document.title, window.location.pathname); } // Check for authentication error indicator if (urlParams.has('auth_error')) { window.bdfrApp.showError('Authentication failed - please check your OAuth configuration'); window.history.replaceState({}, document.title, window.location.pathname); } }); // Handle page visibility changes document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && window.bdfrApp) { window.bdfrApp.updateStatus(); window.bdfrApp.checkAuthentication(); } });