diff --git a/assets/logo.png b/assets/logo.png
index bafd05f..addfbcc 100644
Binary files a/assets/logo.png and b/assets/logo.png differ
diff --git a/public/app.js b/public/app.js
index 42e6d03..c8a1882 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,283 +1,935 @@
-// Global state
-let ws = null
-let logs = []
-let accounts = []
-let status = { running: false }
+/**
+ * Microsoft Rewards Bot Dashboard - Frontend JavaScript
+ *
+ * Handles real-time updates, charts, bot control, and UI interactions
+ */
-// Theme
-function toggleTheme() {
- document.body.classList.toggle('light-theme')
- const icon = document.querySelector('.theme-toggle i')
- if (document.body.classList.contains('light-theme')) {
- icon.className = 'fas fa-sun'
- localStorage.setItem('theme', 'light')
- } else {
- icon.className = 'fas fa-moon'
- localStorage.setItem('theme', 'dark')
- }
+// ============================================================================
+// State Management
+// ============================================================================
+
+const state = {
+ isRunning: false,
+ autoScroll: true,
+ logs: [],
+ accounts: [],
+ stats: {
+ totalAccounts: 0,
+ totalPoints: 0,
+ completed: 0,
+ errors: 0,
+ startTime: null
+ },
+ pointsHistory: [],
+ activityStats: {},
+ currentLogFilter: 'all',
+ ws: null,
+ reconnectAttempts: 0,
+ maxReconnectAttempts: 10,
+ reconnectDelay: 1000
}
-// Load theme
-const savedTheme = localStorage.getItem('theme')
-if (savedTheme === 'light') {
- document.body.classList.add('light-theme')
- document.querySelector('.theme-toggle i').className = 'fas fa-sun'
+// Chart instances
+let pointsChart = null
+let activityChart = null
+
+// ============================================================================
+// Initialization
+// ============================================================================
+
+document.addEventListener('DOMContentLoaded', () => {
+ initializeWebSocket()
+ initializeCharts()
+ loadInitialData()
+ startUptimeTimer()
+ loadThemePreference()
+})
+
+// ============================================================================
+// WebSocket Connection
+// ============================================================================
+
+function initializeWebSocket() {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
+ const wsUrl = `${protocol}//${window.location.host}`
+
+ try {
+ state.ws = new WebSocket(wsUrl)
+
+ state.ws.onopen = () => {
+ updateConnectionStatus(true)
+ state.reconnectAttempts = 0
+ state.reconnectDelay = 1000
+ console.log('[WS] Connected to dashboard server')
+ }
+
+ state.ws.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data)
+ handleWebSocketMessage(data)
+ } catch (error) {
+ console.error('[WS] Failed to parse message:', error)
+ }
+ }
+
+ state.ws.onclose = () => {
+ updateConnectionStatus(false)
+ console.log('[WS] Connection closed, attempting reconnect...')
+ attemptReconnect()
+ }
+
+ state.ws.onerror = (error) => {
+ console.error('[WS] Error:', error)
+ }
+ } catch (error) {
+ console.error('[WS] Failed to connect:', error)
+ attemptReconnect()
+ }
}
-// HTML escaping utility to prevent XSS attacks
-function escapeHtml(text) {
- if (text === null || text === undefined) return ''
- const div = document.createElement('div')
- div.textContent = String(text)
- return div.innerHTML
+function attemptReconnect() {
+ if (state.reconnectAttempts >= state.maxReconnectAttempts) {
+ showToast('Connection lost. Please refresh the page.', 'error')
+ return
+ }
+
+ state.reconnectAttempts++
+ state.reconnectDelay = Math.min(state.reconnectDelay * 1.5, 30000)
+
+ setTimeout(() => {
+ console.log(`[WS] Reconnect attempt ${state.reconnectAttempts}/${state.maxReconnectAttempts}`)
+ initializeWebSocket()
+ }, state.reconnectDelay)
}
-// Toast notification
-function showToast(message, type = 'success') {
- const container = document.getElementById('toastContainer')
- const toast = document.createElement('div')
- toast.className = `toast toast-${type}`
+function handleWebSocketMessage(data) {
+ // Handle init message with all initial data
+ if (data.type === 'init' && data.data) {
+ if (data.data.logs) {
+ data.data.logs.forEach(log => addLogEntry(log))
+ }
+ if (data.data.status) {
+ updateBotStatus(data.data.status)
+ }
+ if (data.data.accounts) {
+ renderAccounts(data.data.accounts)
+ }
+ return
+ }
- const iconMap = {
- success: 'fa-check-circle',
- error: 'fa-exclamation-circle',
- info: 'fa-info-circle'
- }
+ // Handle payload format (from state listener) or data format (from broadcast)
+ const payload = data.payload || data.data || data
- toast.innerHTML = `
-
- ${escapeHtml(message)}
- `
-
- container.appendChild(toast)
-
- setTimeout(() => {
- toast.remove()
- }, 5000)
+ switch (data.type) {
+ case 'log':
+ // Handle both { log: {...} } and direct log object
+ 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
+ case 'points':
+ updatePointsHistory(payload)
+ break
+ case 'activity':
+ updateActivityStats(payload)
+ break
+ default:
+ console.log('[WS] Unknown message type:', data.type)
+ }
}
-// Update UI
-function updateStatus(data) {
- status = data
- const badge = document.getElementById('statusBadge')
- const btnStart = document.getElementById('btnStart')
- const btnStop = document.getElementById('btnStop')
-
- if (data.running) {
- badge.className = 'status-badge status-running'
- badge.textContent = 'RUNNING'
- btnStart.disabled = true
- btnStop.disabled = false
- } else {
- badge.className = 'status-badge status-stopped'
- badge.textContent = 'STOPPED'
- btnStart.disabled = false
- btnStop.disabled = true
- }
+function updateConnectionStatus(connected) {
+ const statusEl = document.getElementById('connectionStatus')
+ if (statusEl) {
+ statusEl.className = `connection-status ${connected ? 'connected' : 'disconnected'}`
+ statusEl.innerHTML = ` ${connected ? 'Connected' : 'Disconnected'}`
+ }
}
-function updateMetrics(data) {
- document.getElementById('totalAccounts').textContent = data.totalAccounts || 0
- document.getElementById('totalPoints').textContent = (data.totalPoints || 0).toLocaleString()
- document.getElementById('completed').textContent = data.accountsCompleted || 0
- document.getElementById('errors').textContent = data.accountsWithErrors || 0
+// ============================================================================
+// Charts Initialization
+// ============================================================================
+
+function initializeCharts() {
+ initPointsChart()
+ initActivityChart()
}
-function updateAccounts(data) {
- accounts = data
- const container = document.getElementById('accountsList')
+function initPointsChart() {
+ const ctx = document.getElementById('pointsChart')
+ if (!ctx) return
- if (data.length === 0) {
- container.innerHTML = '
'
- 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)')
- // SECURITY FIX: Escape all user-provided data to prevent XSS
- container.innerHTML = data.map(acc => `
-
-
-
${escapeHtml(acc.maskedEmail.charAt(0).toUpperCase())}
-
-
${escapeHtml(acc.maskedEmail)}
-
- ${acc.lastSync ? `Last sync: ${escapeHtml(new Date(acc.lastSync).toLocaleString())}` : 'Never synced'}
-
-
-
-
-
-
${acc.points !== undefined ? escapeHtml(acc.points.toLocaleString()) : 'N/A'}
-
Points
-
-
${escapeHtml(acc.status.toUpperCase())}
-
-
- `).join('')
+ pointsChart = new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels: generateDateLabels(7),
+ datasets: [{
+ label: 'Points Earned',
+ data: generatePlaceholderData(7),
+ borderColor: '#58a6ff',
+ backgroundColor: gradient,
+ borderWidth: 2,
+ fill: true,
+ tension: 0.4,
+ pointRadius: 4,
+ pointHoverRadius: 6,
+ pointBackgroundColor: '#58a6ff',
+ pointBorderColor: '#0d1117',
+ pointBorderWidth: 2
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ interaction: {
+ intersect: false,
+ mode: 'index'
+ },
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ backgroundColor: '#161b22',
+ titleColor: '#f0f6fc',
+ bodyColor: '#8b949e',
+ borderColor: '#30363d',
+ borderWidth: 1,
+ padding: 12,
+ displayColors: false,
+ callbacks: {
+ label: (context) => `${context.parsed.y.toLocaleString()} points`
+ }
+ }
+ },
+ scales: {
+ x: {
+ grid: {
+ color: '#21262d',
+ drawBorder: false
+ },
+ ticks: {
+ color: '#8b949e',
+ font: { size: 11 }
+ }
+ },
+ y: {
+ grid: {
+ color: '#21262d',
+ drawBorder: false
+ },
+ ticks: {
+ color: '#8b949e',
+ font: { size: 11 },
+ callback: (value) => value.toLocaleString()
+ },
+ beginAtZero: true
+ }
+ }
+ }
+ })
}
-function addLog(log) {
- logs.push(log)
- if (logs.length > 200) {
- logs.shift()
- }
- renderLogs()
+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,
+ hoverOffset: 8
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ cutout: '65%',
+ plugins: {
+ legend: {
+ position: 'right',
+ labels: {
+ color: '#8b949e',
+ font: { size: 11 },
+ padding: 15,
+ usePointStyle: true,
+ pointStyle: 'circle'
+ }
+ },
+ tooltip: {
+ backgroundColor: '#161b22',
+ titleColor: '#f0f6fc',
+ bodyColor: '#8b949e',
+ borderColor: '#30363d',
+ borderWidth: 1,
+ padding: 12,
+ callbacks: {
+ label: (context) => `${context.label}: ${context.parsed}%`
+ }
+ }
+ }
+ }
+ })
}
-function renderLogs() {
- const container = document.getElementById('logsContainer')
-
- if (logs.length === 0) {
- container.innerHTML = ''
- return
- }
-
- // SECURITY FIX: Escape all log data to prevent XSS
- container.innerHTML = logs.map(log => `
-
- [${escapeHtml(new Date(log.timestamp).toLocaleTimeString())}]
- ${escapeHtml(log.platform)}
- [${escapeHtml(log.title)}]
- ${escapeHtml(log.message)}
-
- `).join('')
-
- // Auto-scroll to bottom
- container.scrollTop = container.scrollHeight
+function generateDateLabels(days) {
+ const labels = []
+ for (let i = days - 1; i >= 0; i--) {
+ const date = new Date()
+ date.setDate(date.getDate() - i)
+ labels.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }))
+ }
+ return labels
}
-// API calls
-async function fetchData() {
- try {
- const [statusRes, accountsRes, metricsRes, logsRes] = await Promise.all([
- fetch('/api/status'),
- fetch('/api/accounts'),
- fetch('/api/metrics'),
- fetch('/api/logs?limit=100')
- ])
-
- updateStatus(await statusRes.json())
- updateAccounts(await accountsRes.json())
- updateMetrics(await metricsRes.json())
- logs = await logsRes.json()
- renderLogs()
- } catch (error) {
- showToast('Failed to fetch data: ' + error.message, 'error')
- }
+function generatePlaceholderData(count) {
+ // Generate realistic-looking placeholder data
+ const data = []
+ let base = 150
+ for (let i = 0; i < count; i++) {
+ base += Math.floor(Math.random() * 50) - 20
+ data.push(Math.max(50, base))
+ }
+ return data
}
+function setChartPeriod(period, btn) {
+ // Update button states
+ document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active'))
+ btn.classList.add('active')
+
+ // Update chart data
+ const days = period === '7d' ? 7 : 30
+ if (pointsChart) {
+ pointsChart.data.labels = generateDateLabels(days)
+ pointsChart.data.datasets[0].data = generatePlaceholderData(days)
+ pointsChart.update('none')
+ }
+}
+
+// ============================================================================
+// Data Loading
+// ============================================================================
+
+async function loadInitialData() {
+ try {
+ const [statusRes, accountsRes] = await Promise.all([
+ fetch('/api/status'),
+ fetch('/api/accounts')
+ ])
+
+ if (statusRes.ok) {
+ const status = await statusRes.json()
+ updateBotStatus(status)
+ }
+
+ if (accountsRes.ok) {
+ const accounts = await accountsRes.json()
+ renderAccounts(accounts)
+ }
+ } catch (error) {
+ console.error('Failed to load initial data:', error)
+ }
+}
+
+async function refreshData() {
+ const btn = document.querySelector('[onclick="refreshData()"]')
+ if (btn) {
+ btn.querySelector('i').classList.add('fa-spin')
+ }
+
+ await loadInitialData()
+
+ setTimeout(() => {
+ if (btn) {
+ btn.querySelector('i').classList.remove('fa-spin')
+ }
+ showToast('Data refreshed', 'success')
+ }, 500)
+}
+
+// ============================================================================
+// Bot Control
+// ============================================================================
+
async function startBot() {
- try {
- showToast('Starting bot...', 'info')
- const res = await fetch('/api/start', { method: 'POST' })
- const data = await res.json()
- if (data.success) {
- showToast(`Bot started! (PID: ${data.pid})`)
- setTimeout(fetchData, 1000)
- } else {
- showToast(data.error || 'Failed to start bot', 'error')
+ try {
+ updateButtonStates(true)
+ const response = await fetch('/api/start', { method: 'POST' })
+ const result = await response.json()
+
+ if (response.ok) {
+ state.isRunning = true
+ state.stats.startTime = Date.now()
+ updateBotStatus({ running: true })
+ showToast('Bot started successfully', 'success')
+ } else {
+ updateButtonStates(false)
+ showToast(result.error || 'Failed to start bot', 'error')
+ }
+ } catch (error) {
+ updateButtonStates(false)
+ showToast('Failed to start bot: ' + error.message, 'error')
}
- } catch (error) {
- showToast('Failed to start bot: ' + error.message, 'error')
- }
}
async function stopBot() {
- try {
- showToast('Stopping bot...', 'info')
- const res = await fetch('/api/stop', { method: 'POST' })
- const data = await res.json()
- if (data.success) {
- showToast('Bot stopped')
- setTimeout(fetchData, 1000)
- } else {
- showToast(data.error || 'Failed to stop bot', 'error')
+ try {
+ const response = await fetch('/api/stop', { method: 'POST' })
+ const result = await response.json()
+
+ if (response.ok) {
+ state.isRunning = false
+ updateBotStatus({ running: false })
+ showToast('Bot stopped', 'info')
+ } else {
+ showToast(result.error || 'Failed to stop bot', 'error')
+ }
+ } catch (error) {
+ showToast('Failed to stop bot: ' + error.message, 'error')
}
- } catch (error) {
- showToast('Failed to stop bot: ' + error.message, 'error')
- }
}
async function restartBot() {
- try {
- showToast('Restarting bot...', 'info')
- const res = await fetch('/api/restart', { method: 'POST' })
- const data = await res.json()
- if (data.success) {
- showToast(`Bot restarted! (PID: ${data.pid})`)
- setTimeout(fetchData, 1000)
- } else {
- showToast(data.error || 'Failed to restart bot', 'error')
- }
- } catch (error) {
- showToast('Failed to restart bot: ' + error.message, 'error')
- }
-}
-
-async function clearLogs() {
- try {
- await fetch('/api/logs', { method: 'DELETE' })
- logs = []
- renderLogs()
- showToast('Logs cleared')
- } catch (error) {
- showToast('Failed to clear logs', 'error')
- }
-}
-
-function refreshData() {
- fetchData()
- showToast('Data refreshed')
-}
-
-// WebSocket
-function connectWebSocket() {
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
- ws = new WebSocket(`${protocol}//${window.location.host}`)
-
- ws.onopen = () => {
- console.log('WebSocket connected')
- }
-
- ws.onmessage = (event) => {
try {
- const data = JSON.parse(event.data)
+ showToast('Restarting bot...', 'info')
+ const response = await fetch('/api/restart', { method: 'POST' })
+ const result = await response.json()
- if (data.type === 'init') {
- logs = data.data.logs || []
- renderLogs()
- if (data.data.status) updateStatus(data.data.status)
- if (data.data.accounts) updateAccounts(data.data.accounts)
- } else if (data.type === 'log') {
- if (data.log) addLog(data.log)
- } else if (data.type === 'status') {
- updateStatus(data.data)
- } else if (data.type === 'accounts') {
- updateAccounts(data.data)
- } else if (data.type === 'account_update') {
- const index = accounts.findIndex(acc => acc.email === data.data.email)
- if (index >= 0) {
- accounts[index] = data.data
+ if (response.ok) {
+ state.stats.startTime = Date.now()
+ showToast('Bot restarted successfully', 'success')
} else {
- accounts.push(data.data)
+ showToast(result.error || 'Failed to restart bot', 'error')
}
- updateAccounts(accounts)
- }
} catch (error) {
- console.error('WebSocket message error:', error)
+ showToast('Failed to restart bot: ' + error.message, 'error')
}
- }
-
- ws.onclose = () => {
- console.log('WebSocket disconnected, reconnecting...')
- setTimeout(connectWebSocket, 3000)
- }
-
- ws.onerror = (error) => {
- console.error('WebSocket error:', error)
- }
}
-// Initialize
-fetchData()
-connectWebSocket()
-setInterval(fetchData, 10000)
+async function resetJobState() {
+ showModal(
+ 'Reset Job State',
+ 'This will clear all completed task records for today, allowing the bot to re-run all activities.
This action cannot be undone.
',
+ [
+ { text: 'Cancel', class: 'btn btn-secondary', onclick: 'closeModal()' },
+ { text: 'Reset', class: 'btn btn-danger', onclick: 'confirmResetJobState()' }
+ ]
+ )
+}
+
+async function confirmResetJobState() {
+ closeModal()
+ try {
+ const response = await fetch('/api/reset-state', { method: 'POST' })
+ const result = await response.json()
+
+ if (response.ok) {
+ showToast('Job state reset successfully', 'success')
+ state.stats.completed = 0
+ state.stats.errors = 0
+ updateStatsDisplay()
+ } else {
+ showToast(result.error || 'Failed to reset state', 'error')
+ }
+ } catch (error) {
+ showToast('Failed to reset state: ' + error.message, 'error')
+ }
+}
+
+function updateButtonStates(running) {
+ const btnStart = document.getElementById('btnStart')
+ const 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)
+
+ const 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()
+ }
+
+ if (status.memory) {
+ document.getElementById('memory').textContent = formatBytes(status.memory)
+ }
+}
+
+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() {
+ document.getElementById('totalAccounts').textContent = state.stats.totalAccounts
+ document.getElementById('totalPoints').textContent = state.stats.totalPoints.toLocaleString()
+ document.getElementById('completed').textContent = state.stats.completed
+ document.getElementById('errors').textContent = state.stats.errors
+
+ // Update accounts badge
+ const badge = document.getElementById('accountsBadge')
+ if (badge) badge.textContent = state.stats.totalAccounts
+}
+
+function updatePointsHistory(data) {
+ if (pointsChart && data.labels && data.values) {
+ pointsChart.data.labels = data.labels
+ pointsChart.data.datasets[0].data = data.values
+ pointsChart.update('none')
+ }
+}
+
+function updateActivityStats(data) {
+ if (activityChart && data.labels && data.values) {
+ activityChart.data.labels = data.labels
+ activityChart.data.datasets[0].data = data.values
+ activityChart.update('none')
+ }
+}
+
+// ============================================================================
+// Accounts Management
+// ============================================================================
+
+function renderAccounts(accounts) {
+ state.accounts = accounts
+ state.stats.totalAccounts = accounts.length
+
+ const container = document.getElementById('accountsList')
+ if (!container) return
+
+ if (accounts.length === 0) {
+ container.innerHTML = 'No accounts configured
'
+ return
+ }
+
+ container.innerHTML = accounts.map(account => {
+ const initial = (account.email || 'U')[0].toUpperCase()
+ const displayEmail = account.email ? maskEmail(account.email) : 'Unknown'
+ const statusClass = account.status || 'pending'
+ const statusText = statusClass.charAt(0).toUpperCase() + statusClass.slice(1)
+
+ return `
+
+
+
${initial}
+
${displayEmail}
+
+
${statusText}
+
+ `
+ }).join('')
+
+ updateStatsDisplay()
+}
+
+function updateAccountStatus(data) {
+ const accountEl = document.querySelector(`.account-item[data-email="${data.email}"]`)
+ if (accountEl) {
+ const 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'
+ const [local, domain] = email.split('@')
+ if (!domain) return email
+ const masked = local.length > 3
+ ? local.slice(0, 2) + '***' + local.slice(-1)
+ : local[0] + '***'
+ return `${masked}@${domain}`
+}
+
+// ============================================================================
+// Logging
+// ============================================================================
+
+function addLogEntry(log) {
+ const container = document.getElementById('logsContainer')
+ if (!container) return
+
+ // Remove empty state message
+ const emptyMsg = container.querySelector('.log-empty')
+ if (emptyMsg) emptyMsg.remove()
+
+ // Normalize log format (server uses 'title' and 'platform', frontend uses 'source')
+ const normalizedLog = {
+ timestamp: log.timestamp || new Date().toISOString(),
+ level: log.level || 'log',
+ source: log.source || log.title || 'BOT',
+ message: log.message || '',
+ platform: log.platform
+ }
+
+ // Store log
+ state.logs.push(normalizedLog)
+
+ // Check filter
+ if (state.currentLogFilter !== 'all' && normalizedLog.level !== state.currentLogFilter) {
+ return
+ }
+
+ // Create log entry
+ const entry = document.createElement('div')
+ entry.className = 'log-entry'
+ entry.innerHTML = `
+ ${formatTime(normalizedLog.timestamp)}
+ ${normalizedLog.level}
+ [${normalizedLog.source}]
+ ${escapeHtml(normalizedLog.message)}
+
+ // Auto-scroll
+ if (state.autoScroll) {
+ container.scrollTop = container.scrollHeight
+ }
+}
+
+function filterLogs() {
+ const filter = document.getElementById('logFilter')
+ if (!filter) return
+
+ state.currentLogFilter = filter.value
+
+ const container = document.getElementById('logsContainer')
+ if (!container) return
+
+ // Clear and re-render
+ container.innerHTML = ''
+
+ const filteredLogs = state.currentLogFilter === 'all'
+ ? state.logs
+ : state.logs.filter(log => log.level === state.currentLogFilter)
+
+ if (filteredLogs.length === 0) {
+ container.innerHTML = 'No logs to display
'
+ return
+ }
+
+ filteredLogs.forEach(log => {
+ const entry = document.createElement('div')
+ entry.className = 'log-entry'
+ entry.innerHTML = `
+ < span class="log-time" > ${ formatTime(log.timestamp || new Date()) }
+ ${log.level || 'log'}
+ [${log.source || 'BOT'}]
+ ${escapeHtml(log.message || '')}
+ `
+ container.appendChild(entry)
+ })
+}
+
+function clearLogs() {
+ state.logs = []
+ const container = document.getElementById('logsContainer')
+ if (container) {
+ container.innerHTML = 'No logs to display
'
+ }
+ showToast('Logs cleared', 'info')
+}
+
+function toggleAutoScroll() {
+ state.autoScroll = !state.autoScroll
+ const 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
+ }
+
+ const options = state.accounts.map(a =>
+ `< option value = "${a.email}" > ${ maskEmail(a.email) } `
+ ).join('')
+
+ showModal(
+ 'Run Single Account',
+ `< p style = "margin-bottom: 1rem;" > Select an account to run:
+ `,
+ [
+ { text: 'Cancel', class: 'btn btn-secondary', onclick: 'closeModal()' },
+ { text: 'Run', class: 'btn btn-primary', onclick: 'executeSingleAccount()' }
+ ]
+ )
+}
+
+async function executeSingleAccount() {
+ const select = document.getElementById('singleAccountSelect')
+ if (!select) return
+
+ const email = select.value
+ closeModal()
+
+ try {
+ showToast(`Running account: ${ maskEmail(email) } `, 'info')
+ // API call would go here
+ // await fetch('/api/run-single', { method: 'POST', body: JSON.stringify({ email }) })
+ } catch (error) {
+ showToast('Failed to run account: ' + error.message, 'error')
+ }
+}
+
+function exportLogs() {
+ if (state.logs.length === 0) {
+ showToast('No logs to export', 'warning')
+ return
+ }
+
+ const logText = state.logs.map(log =>
+ `[${ formatTime(log.timestamp) }][${ log.level?.toUpperCase() || 'LOG' }][${ log.source || 'BOT' }] ${ log.message } `
+ ).join('\n')
+
+ const blob = new Blob([logText], { type: 'text/plain' })
+ const url = URL.createObjectURL(blob)
+ const 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 = 'info') {
+ const container = document.getElementById('toastContainer')
+ if (!container) return
+
+ const toast = document.createElement('div')
+ toast.className = `toast ${ type } `
+
+ const icons = {
+ success: 'fa-check-circle',
+ error: 'fa-times-circle',
+ warning: 'fa-exclamation-circle',
+ info: 'fa-info-circle'
+ }
+
+ toast.innerHTML = `
+ < i class="fas ${icons[type] || icons.info}" >
+ ${message}
+ `
+
+ container.appendChild(toast)
+
+ setTimeout(() => {
+ toast.style.animation = 'slideIn 0.3s ease reverse'
+ setTimeout(() => toast.remove(), 300)
+ }, 4000)
+}
+
+function showModal(title, body, buttons = []) {
+ const modal = document.getElementById('modal')
+ const modalTitle = document.getElementById('modalTitle')
+ const modalBody = document.getElementById('modalBody')
+ const modalFooter = document.getElementById('modalFooter')
+
+ if (!modal || !modalTitle || !modalBody || !modalFooter) return
+
+ modalTitle.textContent = title
+ modalBody.innerHTML = body
+
+ modalFooter.innerHTML = buttons.map(btn =>
+ `< button class="${btn.class}" onclick = "${btn.onclick}" > ${ btn.text } `
+ ).join('')
+
+ modal.classList.add('show')
+}
+
+function closeModal() {
+ const modal = document.getElementById('modal')
+ if (modal) modal.classList.remove('show')
+}
+
+// Close modal on backdrop click
+document.addEventListener('click', (e) => {
+ if (e.target.classList.contains('modal')) {
+ closeModal()
+ }
+})
+
+// Close modal on Escape
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ closeModal()
+ }
+})
+
+// ============================================================================
+// Theme
+// ============================================================================
+
+function toggleTheme() {
+ document.body.classList.toggle('light-theme')
+ const isLight = document.body.classList.contains('light-theme')
+ localStorage.setItem('theme', isLight ? 'light' : 'dark')
+
+ // Update icon
+ const btn = document.querySelector('.theme-toggle i')
+ if (btn) {
+ btn.className = isLight ? 'fas fa-sun' : 'fas fa-moon'
+ }
+
+ // Update charts for theme
+ updateChartsTheme(isLight)
+}
+
+function loadThemePreference() {
+ const theme = localStorage.getItem('theme')
+ if (theme === 'light') {
+ document.body.classList.add('light-theme')
+ const btn = document.querySelector('.theme-toggle i')
+ if (btn) btn.className = 'fas fa-sun'
+ updateChartsTheme(true)
+ }
+}
+
+function updateChartsTheme(isLight) {
+ const gridColor = isLight ? '#eaeef2' : '#21262d'
+ const 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) {
+ const elapsed = Date.now() - state.stats.startTime
+ document.getElementById('uptime').textContent = formatDuration(elapsed)
+ }
+ }, 1000)
+}
+
+// ============================================================================
+// Formatting Utilities
+// ============================================================================
+
+function formatTime(timestamp) {
+ const date = new Date(timestamp)
+ return date.toLocaleTimeString('en-US', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ })
+}
+
+function formatDuration(ms) {
+ const seconds = Math.floor(ms / 1000)
+ const hours = Math.floor(seconds / 3600)
+ const minutes = Math.floor((seconds % 3600) / 60)
+ const secs = seconds % 60
+
+ return [hours, minutes, secs]
+ .map(v => v.toString().padStart(2, '0'))
+ .join(':')
+}
+
+function formatBytes(bytes) {
+ if (bytes === 0) return '0 MB'
+ const mb = bytes / (1024 * 1024)
+ return `${ mb.toFixed(1) } MB`
+}
+
+function escapeHtml(text) {
+ const div = document.createElement('div')
+ div.textContent = text
+ return div.innerHTML
+}
+
+// ============================================================================
+// Global Error Handler
+// ============================================================================
+
+window.onerror = (message, source, lineno, colno, error) => {
+ console.error('Global error:', { message, source, lineno, colno, error })
+ return false
+}
+
+window.onunhandledrejection = (event) => {
+ console.error('Unhandled rejection:', event.reason)
+}
diff --git a/public/index.html b/public/index.html
index 21e4639..3151a1b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,47 +1,229 @@
+
Microsoft Rewards Bot Dashboard
+
+
+