/** * 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 = '
Charts unavailable (Chart.js blocked by browser)
' } if (activityCanvas) { activityCanvas.parentElement.innerHTML = '
Charts unavailable (Chart.js blocked by browser)
' } return } initPointsChart() initActivityChart() } function initPointsChart() { const ctx = document.getElementById('pointsChart') if (!ctx) return const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 250) gradient.addColorStop(0, 'rgba(88, 166, 255, 0.3)') gradient.addColorStop(1, 'rgba(88, 166, 255, 0)') pointsChart = new Chart(ctx, { type: 'line', data: { labels: generateDateLabels(7), datasets: [{ label: 'Points', data: new Array(7).fill(0), // Real data loaded from API borderColor: '#58a6ff', backgroundColor: gradient, borderWidth: 2, fill: true, tension: 0.4, pointRadius: 4, pointBackgroundColor: '#58a6ff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { color: '#21262d' }, ticks: { color: '#8b949e' } }, y: { grid: { color: '#21262d' }, ticks: { color: '#8b949e' }, beginAtZero: true } } } }) } function initActivityChart() { const ctx = document.getElementById('activityChart') if (!ctx) return activityChart = new Chart(ctx, { type: 'doughnut', data: { labels: ['Searches', 'Daily Set', 'Punch Cards', 'Quizzes', 'Other'], datasets: [{ data: [45, 25, 15, 10, 5], backgroundColor: ['#58a6ff', '#3fb950', '#d29922', '#a371f7', '#39c5cf'], borderColor: '#161b22', borderWidth: 3 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '65%', plugins: { legend: { position: 'right', labels: { color: '#8b949e', font: { size: 11 } } } } } }) } function generateDateLabels(days) { const labels = [] for (let i = days - 1; i >= 0; i--) { const d = new Date() d.setDate(d.getDate() - i) labels.push(d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })) } return labels } function setChartPeriod(period, btn) { document.querySelectorAll('.period-btn').forEach((b) => { b.classList.remove('active') }) btn.classList.add('active') const days = period === '7d' ? 7 : 30 if (pointsChart) { pointsChart.data.labels = generateDateLabels(days) // Fetch actual data from account history fetch('/api/account-history') .then(r => r.json()) .then(history => { const pointsData = extractPointsFromHistory(history, days) pointsChart.data.datasets[0].data = pointsData pointsChart.update('none') }) .catch(() => { // Fallback to zeros if API fails pointsChart.data.datasets[0].data = new Array(days).fill(0) pointsChart.update('none') }) } } function extractPointsFromHistory(history, days) { const dataByDate = {} const today = new Date() // Initialize all days with 0 for (let i = 0; i < days; i++) { const d = new Date(today) d.setDate(d.getDate() - i) const key = d.toISOString().slice(0, 10) dataByDate[key] = 0 } // Fill with actual data for (const email in history) { const accountHistory = history[email] for (const day in accountHistory) { if (dataByDate.hasOwnProperty(day)) { const dayData = accountHistory[day] dataByDate[day] += dayData.pointsEarnedToday || 0 } } } // Convert to array (reverse chronological) const result = [] for (let i = days - 1; i >= 0; i--) { const d = new Date(today) d.setDate(d.getDate() - i) const key = d.toISOString().slice(0, 10) result.push(dataByDate[key] || 0) } return result } // Data Loading function loadInitialData() { fetch('/api/status').then((r) => { return r.json() }).then(updateBotStatus).catch(() => { }) fetch('/api/accounts').then((r) => { return r.json() }).then(renderAccounts).catch(() => { }) loadAccountHistoryData() // FIXED: Load real historical data for charts } // FIXED: Load account history from API to populate charts with real data function loadAccountHistoryData() { fetch('/api/account-history') .then((r) => r.json()) .then((histories) => { if (histories && Object.keys(histories).length > 0) { updateChartsWithRealData(histories) } }) .catch((e) => { console.warn('[History] Failed to load:', e) }) } // FIXED: Update charts with real historical data function updateChartsWithRealData(histories) { var last7Days = {} var activityCounts = { 'Desktop Search': 0, 'Mobile Search': 0, 'Daily Set': 0, 'Quizzes': 0, 'Other': 0 } // Process each account's history Object.values(histories).forEach((accountHistory) => { if (!accountHistory.history) return accountHistory.history.forEach((entry) => { var date = entry.date || entry.timestamp.split('T')[0] // Aggregate points by day if (!last7Days[date]) { last7Days[date] = 0 } last7Days[date] += entry.totalPoints || 0 // Count activity types if (entry.completedActivities) { entry.completedActivities.forEach((activity) => { if (activity.includes('Search')) { if (entry.desktopPoints > 0) activityCounts['Desktop Search']++ if (entry.mobilePoints > 0) activityCounts['Mobile Search']++ } else if (activity.includes('DailySet')) { activityCounts['Daily Set']++ } else if (activity.includes('Quiz') || activity.includes('Poll')) { activityCounts['Quizzes']++ } else { activityCounts['Other']++ } }) } }) }) // Update points chart with last 7 days if (pointsChart) { var sortedDates = Object.keys(last7Days).sort().slice(-7) var labels = sortedDates.map((d) => { var date = new Date(d) return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) }) var data = sortedDates.map((d) => { return last7Days[d] }) pointsChart.data.labels = labels.length > 0 ? labels : generateDateLabels(7) pointsChart.data.datasets[0].data = data.length > 0 ? data : new Array(7).fill(0) pointsChart.update() } // Update activity chart if (activityChart) { var total = Object.values(activityCounts).reduce((sum, val) => { return sum + val }, 0) if (total > 0) { activityChart.data.labels = Object.keys(activityCounts) activityChart.data.datasets[0].data = Object.values(activityCounts) activityChart.update() } } } function refreshData() { var btn = document.querySelector('[onclick="refreshData()"] i') if (btn) btn.classList.add('fa-spin') loadInitialData() setTimeout(() => { if (btn) btn.classList.remove('fa-spin') showToast('Data refreshed', 'success') }, 500) } // Bot Control function startBot() { updateButtonStates(true) fetch('/api/start', { method: 'POST' }) .then((r) => { return r.json() }) .then((result) => { if (result.success || result.pid) { state.isRunning = true state.stats.startTime = Date.now() updateBotStatus({ running: true }) showToast('Bot started', 'success') } else { updateButtonStates(false) showToast(result.error || 'Failed to start', 'error') } }) .catch((e) => { updateButtonStates(false) showToast('Failed: ' + e.message, 'error') }) } function stopBot() { fetch('/api/stop', { method: 'POST' }) .then((r) => { return r.json() }) .then((result) => { if (result.success) { state.isRunning = false updateBotStatus({ running: false }) showToast('Bot stopped', 'info') } else { showToast(result.error || 'Failed to stop', 'error') } }) .catch((e) => { showToast('Failed: ' + e.message, 'error') }) } function restartBot() { showToast('Restarting...', 'info') fetch('/api/restart', { method: 'POST' }) .then((r) => { return r.json() }) .then((result) => { if (result.success) { state.stats.startTime = Date.now() showToast('Bot restarted', 'success') } else { showToast(result.error || 'Failed to restart', 'error') } }) .catch((e) => { showToast('Failed: ' + e.message, 'error') }) } function resetJobState() { showModal('Reset Job State', '

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 = '
No accounts configured
' return } var html = '' accounts.forEach((account) => { var initial = (account.email || 'U')[0].toUpperCase() var displayEmail = account.email ? maskEmail(account.email) : 'Unknown' var statusClass = account.status || 'pending' var statusText = statusClass.charAt(0).toUpperCase() + statusClass.slice(1) html += '
' + '
' + '' + '' + '
' + '' + statusText + '' + '
' }) container.innerHTML = html updateStatsDisplay() } function updateAccountStatus(data) { var accountEl = document.querySelector('.account-item[data-email="' + data.email + '"]') if (accountEl) { var statusEl = accountEl.querySelector('.account-status') if (statusEl) { statusEl.className = 'account-status ' + data.status statusEl.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1) } } } function maskEmail(email) { if (!email) return 'Unknown' var parts = email.split('@') if (parts.length < 2) return email var local = parts[0] var domain = parts[1] var masked = local.length > 3 ? local.slice(0, 2) + '***' + local.slice(-1) : local[0] + '***' return masked + '@' + domain } // Logging function addLogEntry(log) { var container = document.getElementById('logsContainer') if (!container) return var emptyMsg = container.querySelector('.log-empty') if (emptyMsg) emptyMsg.remove() var normalizedLog = { timestamp: log.timestamp || new Date().toISOString(), level: log.level || 'log', source: log.source || log.title || 'BOT', message: log.message || '' } state.logs.push(normalizedLog) if (state.currentLogFilter !== 'all' && normalizedLog.level !== state.currentLogFilter) { return } var entry = document.createElement('div') entry.className = 'log-entry' entry.innerHTML = '' + formatTime(normalizedLog.timestamp) + '' + '' + normalizedLog.level + '' + '[' + normalizedLog.source + ']' + '' + escapeHtml(normalizedLog.message) + '' container.appendChild(entry) while (container.children.length > 500) { container.removeChild(container.firstChild) } if (state.autoScroll) { container.scrollTop = container.scrollHeight } } function filterLogs() { var filter = document.getElementById('logFilter') if (!filter) return state.currentLogFilter = filter.value var container = document.getElementById('logsContainer') if (!container) return container.innerHTML = '' var filtered = state.currentLogFilter === 'all' ? state.logs : state.logs.filter((log) => { return log.level === state.currentLogFilter }) if (filtered.length === 0) { container.innerHTML = '
No logs to display
' return } filtered.forEach((log) => { var entry = document.createElement('div') entry.className = 'log-entry' entry.innerHTML = '' + formatTime(log.timestamp) + '' + '' + log.level + '' + '[' + log.source + ']' + '' + escapeHtml(log.message) + '' container.appendChild(entry) }) } function clearLogs() { state.logs = [] var container = document.getElementById('logsContainer') if (container) { container.innerHTML = '
No logs to display
' } showToast('Logs cleared', 'info') } function toggleAutoScroll() { state.autoScroll = !state.autoScroll var btn = document.getElementById('btnAutoScroll') if (btn) { btn.classList.toggle('btn-primary', state.autoScroll) btn.classList.toggle('btn-secondary', !state.autoScroll) } showToast('Auto-scroll ' + (state.autoScroll ? 'enabled' : 'disabled'), 'info') } // Quick Actions function runSingleAccount() { if (state.accounts.length === 0) { showToast('No accounts available', 'warning') return } var options = state.accounts.map((a) => { return '' }).join('') showModal('Run Single Account', '

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() { showModal('Configuration Viewer', `
Loading configuration...
`, []) // Fetch config (read-only view) fetch('/api/config') .then(r => r.json()) .then(data => { const body = `
⚠️ Read-Only View
This is a simplified preview. To edit config:
1. Open src/config.jsonc in a text editor
2. Make your changes
3. Save and restart the bot

💡 Manual editing preserves comments and complex settings

` const buttons = [ { cls: 'btn btn-sm btn-secondary', action: 'closeModal()', text: 'Close' } ] showModal('Configuration Viewer', body, buttons) }) .catch(e => { showToast('Failed to load config: ' + e.message, 'error') closeModal() }) } function saveConfig() { // Config editing is disabled - this function is now unused showToast('Config editing disabled. Edit src/config.jsonc manually.', 'warning') closeModal() } function viewHistory() { showModal('Run History', '
Loading history...
', []) fetch('/api/history') .then(r => r.json()) .then(history => { if (!history || history.length === 0) { showModal('Run History', '

No history available yet

', [ { cls: 'btn btn-sm btn-secondary', action: 'closeModal()', text: 'Close' } ]) return } const rows = history.slice(0, 10).map(h => `
${new Date(h.timestamp || Date.now()).toLocaleString()}
✅ ${h.successCount || 0} success ❌ ${h.errorCount || 0} errors 🎯 ${h.totalPoints || 0} pts
`).join('') const body = `
${rows}
` const buttons = [ { cls: 'btn btn-sm btn-secondary', action: 'closeModal()', text: 'Close' } ] showModal('Run History (Last 10 Runs)', body, buttons) }) .catch(e => { showToast('Failed to load history: ' + e.message, 'error') closeModal() }) } // 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() })