/** * Offline Manager * Handles offline detection and feedback queueing for the mobile app */ (function() { 'use strict'; const OFFLINE_QUEUE_KEY = 'bbplease_offline_feedback_queue'; const SYNC_INTERVAL = 30000; // Sync every 30 seconds when online let syncIntervalId = null; /** * Initialize offline manager */ function init() { // Listen for online/offline events window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); // Check initial state if (navigator.onLine) { syncPendingFeedback(); startSyncInterval(); } else { handleOffline(); } // Try to sync on page load syncPendingFeedback(); } /** * Handle going online */ function handleOnline() { console.log('📶 Online - syncing pending feedback...'); syncPendingFeedback(); startSyncInterval(); // Show notification if there was pending feedback const queue = getQueue(); if (queue.length > 0) { showToast(`Syncing ${queue.length} pending feedback...`); } } /** * Handle going offline */ function handleOffline() { console.log('📴 Offline - feedback will be queued'); stopSyncInterval(); showToast('You are offline. Feedback will be saved and synced when online.'); } /** * Start periodic sync interval */ function startSyncInterval() { if (syncIntervalId) { clearInterval(syncIntervalId); } syncIntervalId = setInterval(() => { if (navigator.onLine) { syncPendingFeedback(); } }, SYNC_INTERVAL); } /** * Stop sync interval */ function stopSyncInterval() { if (syncIntervalId) { clearInterval(syncIntervalId); syncIntervalId = null; } } /** * Get offline queue from localStorage */ function getQueue() { try { const queueJson = localStorage.getItem(OFFLINE_QUEUE_KEY); return queueJson ? JSON.parse(queueJson) : []; } catch (e) { console.error('Error reading offline queue:', e); return []; } } /** * Save queue to localStorage */ function saveQueue(queue) { try { localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue)); } catch (e) { console.error('Error saving offline queue:', e); } } /** * Add feedback to offline queue */ function queueFeedback(feedbackData) { const queue = getQueue(); const feedbackItem = { ...feedbackData, queued_at: new Date().toISOString(), id: feedbackData.audio_id || Date.now().toString() }; queue.push(feedbackItem); saveQueue(queue); console.log('📦 Feedback queued offline:', feedbackItem.id); return feedbackItem.id; } /** * Remove feedback from queue */ function removeFromQueue(feedbackId) { const queue = getQueue(); const filtered = queue.filter(item => item.id !== feedbackId); saveQueue(filtered); return queue.length - filtered.length; // Return number removed } /** * Sync pending feedback when online */ async function syncPendingFeedback() { if (!navigator.onLine) { return { synced: 0, failed: 0 }; } const queue = getQueue(); if (queue.length === 0) { return { synced: 0, failed: 0 }; } console.log(`🔄 Syncing ${queue.length} queued feedback items...`); let synced = 0; let failed = 0; const failedItems = []; for (const item of queue) { try { const url = (window.APP_CONFIG && window.APP_CONFIG.getApiUrl) ? window.APP_CONFIG.getApiUrl('/feedback') : '/feedback'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ audio_id: item.audio_id, predicted_label: item.predicted_label, correct_label: item.correct_label, is_correct: item.is_correct }) }); if (response.ok) { removeFromQueue(item.id); synced++; console.log('✅ Synced feedback:', item.id); } else { failed++; failedItems.push(item); console.warn('❌ Failed to sync feedback:', item.id, response.status); } } catch (error) { failed++; failedItems.push(item); console.error('❌ Error syncing feedback:', item.id, error); } } // If some failed, keep them in queue but update the queue if (failed > 0 && synced > 0) { // Remove only successfully synced items const remaining = queue.filter(item => failedItems.some(failed => failed.id === item.id) ); saveQueue(remaining); } if (synced > 0) { console.log(`✅ Synced ${synced} feedback items`); if (typeof showToast === 'function') { showToast(`Synced ${synced} feedback items`); } } return { synced, failed }; } /** * Check if currently online */ function isOnline() { return navigator.onLine !== false; } /** * Get queue status */ function getQueueStatus() { const queue = getQueue(); return { count: queue.length, items: queue, isOnline: isOnline() }; } /** * Clear queue (use with caution) */ function clearQueue() { saveQueue([]); console.log('🗑️ Offline queue cleared'); } // Export to window window.OfflineManager = { init: init, queueFeedback: queueFeedback, syncPendingFeedback: syncPendingFeedback, getQueueStatus: getQueueStatus, clearQueue: clearQueue, isOnline: isOnline }; // Auto-initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();