/** * Microsoft Rewards Bot Dashboard - Frontend JavaScript */ // State const state = { isRunning: false, autoScroll: true, logs: [], accounts: [], stats: { totalAccounts: 0, totalPoints: 0, completed: 0, errors: 0, startTime: null }, currentLogFilter: 'all', ws: null, reconnectAttempts: 0 } let pointsChart = null let activityChart = null // Initialize on DOM ready document.addEventListener('DOMContentLoaded', () => { initWebSocket() initCharts() loadInitialData() startUptimeTimer() loadTheme() }) // WebSocket function initWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const wsUrl = protocol + '//' + window.location.host try { state.ws = new WebSocket(wsUrl) state.ws.onopen = function () { updateConnectionStatus(true) state.reconnectAttempts = 0 console.log('[WS] Connected') } state.ws.onmessage = function (event) { try { handleWsMessage(JSON.parse(event.data)) } catch (e) { console.error('[WS] Parse error:', e) } } state.ws.onclose = function () { updateConnectionStatus(false) attemptReconnect() } state.ws.onerror = function (e) { console.error('[WS] Error:', e) } } catch (e) { console.error('[WS] Failed:', e) attemptReconnect() } } function attemptReconnect() { if (state.reconnectAttempts >= 10) { showToast('Connection lost. Please refresh.', 'error') return } state.reconnectAttempts++ setTimeout(initWebSocket, Math.min(1000 * Math.pow(1.5, state.reconnectAttempts), 30000)) } function handleWsMessage(data) { if (data.type === 'init' && data.data) { if (data.data.logs) data.data.logs.forEach(addLogEntry) if (data.data.status) updateBotStatus(data.data.status) if (data.data.accounts) renderAccounts(data.data.accounts) return } const payload = data.payload || data.data || data switch (data.type) { case 'log': addLogEntry(payload.log || payload) break case 'status': updateBotStatus(payload) break case 'stats': updateStats(payload) break case 'account': case 'account_update': updateAccountStatus(payload) break case 'accounts': renderAccounts(payload) break } } function updateConnectionStatus(connected) { const el = document.getElementById('connectionStatus') if (el) { el.className = 'connection-status ' + (connected ? 'connected' : 'disconnected') el.innerHTML = ' ' + (connected ? 'Connected' : 'Disconnected') } } // Charts function initCharts() { // FIXED: Fallback if Chart.js blocked by tracking prevention if (typeof Chart === 'undefined') { console.warn('[Charts] Chart.js not loaded (may be blocked by tracking prevention)') var pointsCanvas = document.getElementById('pointsChart') var activityCanvas = document.getElementById('activityChart') if (pointsCanvas) { pointsCanvas.parentElement.innerHTML = '
This will clear all completed task records for today.
' + '' + ' This cannot be undone.
', [ { text: 'Cancel', cls: 'btn btn-secondary', action: 'closeModal()' }, { text: 'Reset', cls: 'btn btn-danger', action: 'confirmResetJobState()' } ] ) } function confirmResetJobState() { closeModal() fetch('/api/reset-state', { method: 'POST' }) .then((r) => { return r.json() }) .then((result) => { if (result.success) { showToast('Job state reset', 'success') state.stats.completed = 0 state.stats.errors = 0 updateStatsDisplay() } else { showToast(result.error || 'Failed to reset', 'error') } }) .catch((e) => { showToast('Failed: ' + e.message, 'error') }) } function updateButtonStates(running) { var btnStart = document.getElementById('btnStart') var btnStop = document.getElementById('btnStop') if (btnStart) btnStart.disabled = running if (btnStop) btnStop.disabled = !running } // Status Updates function updateBotStatus(status) { state.isRunning = status.running updateButtonStates(status.running) var badge = document.getElementById('statusBadge') if (badge) { badge.className = 'status-badge ' + (status.running ? 'status-running' : 'status-stopped') badge.innerHTML = '' + (status.running ? 'RUNNING' : 'STOPPED') + '' } if (status.startTime) { state.stats.startTime = new Date(status.startTime).getTime() } } function updateStats(stats) { if (stats.totalAccounts !== undefined) state.stats.totalAccounts = stats.totalAccounts if (stats.totalPoints !== undefined) state.stats.totalPoints = stats.totalPoints if (stats.completed !== undefined) state.stats.completed = stats.completed if (stats.errors !== undefined) state.stats.errors = stats.errors updateStatsDisplay() } function updateStatsDisplay() { var el el = document.getElementById('totalAccounts') if (el) el.textContent = state.stats.totalAccounts el = document.getElementById('totalPoints') if (el) el.textContent = state.stats.totalPoints.toLocaleString() el = document.getElementById('completed') if (el) el.textContent = state.stats.completed el = document.getElementById('errors') if (el) el.textContent = state.stats.errors el = document.getElementById('accountsBadge') if (el) el.textContent = state.stats.totalAccounts } // Accounts function renderAccounts(accounts) { state.accounts = accounts state.stats.totalAccounts = accounts.length var container = document.getElementById('accountsList') if (!container) return if (accounts.length === 0) { container.innerHTML = 'Select an account to run:
' + '', [ { text: 'Cancel', cls: 'btn btn-secondary', action: 'closeModal()' }, { text: 'Run', cls: 'btn btn-primary', action: 'executeSingleAccount()' } ] ) } function executeSingleAccount() { var select = document.getElementById('singleAccountSelect') if (!select) return var email = select.value closeModal() if (!email) { showToast('No account selected', 'error') return } showToast('Starting bot for: ' + maskEmail(email), 'info') // Call API to run single account fetch('/api/run-single', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email }) }) .then((res) => res.json()) .then((data) => { if (data.success) { showToast('✓ Bot started for account: ' + maskEmail(email), 'success') refreshData() // FIXED: Use refreshData() instead of undefined loadStatus() } else { showToast('✗ Failed to start: ' + (data.error || 'Unknown error'), 'error') } }) .catch((err) => { console.error('[API] Run single failed:', err) showToast('✗ Request failed: ' + err.message, 'error') }) } function exportLogs() { if (state.logs.length === 0) { showToast('No logs to export', 'warning') return } var logText = state.logs.map((log) => { return '[' + formatTime(log.timestamp) + '] [' + (log.level || 'LOG').toUpperCase() + '] [' + (log.source || 'BOT') + '] ' + log.message }).join('\n') var blob = new Blob([logText], { type: 'text/plain' }) var url = URL.createObjectURL(blob) var a = document.createElement('a') a.href = url a.download = 'rewards-bot-logs-' + new Date().toISOString().slice(0, 10) + '.txt' a.click() URL.revokeObjectURL(url) showToast('Logs exported', 'success') } function openConfig() { showToast('Config editor coming soon', 'info') } function viewHistory() { showToast('History viewer coming soon', 'info') } // UI Utilities function showToast(message, type) { type = type || 'info' var container = document.getElementById('toastContainer') if (!container) return var toast = document.createElement('div') toast.className = 'toast ' + type var icons = { success: 'fa-check-circle', error: 'fa-times-circle', warning: 'fa-exclamation-circle', info: 'fa-info-circle' } toast.innerHTML = '' + message + '' container.appendChild(toast) setTimeout(() => { toast.style.animation = 'slideIn 0.3s ease reverse' setTimeout(() => { toast.remove() }, 300) }, 4000) } function showModal(title, body, buttons) { var modal = document.getElementById('modal') var modalTitle = document.getElementById('modalTitle') var modalBody = document.getElementById('modalBody') var modalFooter = document.getElementById('modalFooter') if (!modal || !modalTitle || !modalBody || !modalFooter) return modalTitle.textContent = title modalBody.innerHTML = body var footerHtml = ''; (buttons || []).forEach((btn) => { footerHtml += '' }) modalFooter.innerHTML = footerHtml modal.classList.add('show') } function closeModal() { var modal = document.getElementById('modal') if (modal) modal.classList.remove('show') } // Theme function toggleTheme() { document.body.classList.toggle('light-theme') var isLight = document.body.classList.contains('light-theme') try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch (e) { } var btn = document.querySelector('.theme-toggle i') if (btn) btn.className = isLight ? 'fas fa-sun' : 'fas fa-moon' updateChartsTheme(isLight) } function loadTheme() { try { var theme = localStorage.getItem('theme') if (theme === 'light') { document.body.classList.add('light-theme') var btn = document.querySelector('.theme-toggle i') if (btn) btn.className = 'fas fa-sun' updateChartsTheme(true) } } catch (e) { } } function updateChartsTheme(isLight) { var gridColor = isLight ? '#eaeef2' : '#21262d' var textColor = isLight ? '#656d76' : '#8b949e' if (pointsChart) { pointsChart.options.scales.x.grid.color = gridColor pointsChart.options.scales.y.grid.color = gridColor pointsChart.options.scales.x.ticks.color = textColor pointsChart.options.scales.y.ticks.color = textColor pointsChart.update('none') } if (activityChart) { activityChart.options.plugins.legend.labels.color = textColor activityChart.update('none') } } // Uptime Timer function startUptimeTimer() { setInterval(() => { if (state.isRunning && state.stats.startTime) { var elapsed = Date.now() - state.stats.startTime var el = document.getElementById('uptime') if (el) el.textContent = formatDuration(elapsed) } }, 1000) } // Formatting function formatTime(timestamp) { var d = new Date(timestamp) return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) } function formatDuration(ms) { var secs = Math.floor(ms / 1000) var hrs = Math.floor(secs / 3600) var mins = Math.floor((secs % 3600) / 60) var s = secs % 60 return pad(hrs) + ':' + pad(mins) + ':' + pad(s) } function pad(n) { return n < 10 ? '0' + n : '' + n } function escapeHtml(text) { var div = document.createElement('div') div.textContent = text return div.innerHTML } // Event listeners document.addEventListener('click', (e) => { if (e.target.classList.contains('modal')) closeModal() }) document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal() })