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 = '

No accounts configured

' - 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 => ` -
-
- - -
-
- - -
-
- `).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 = '

No logs yet...

' - 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 ` +
+
+ + +
+ ${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 + +
+
-

🎯 Microsoft Rewards Bot

-
- -
STOPPED
+
+

🎯 Microsoft Rewards Bot

+ v3.5.0 +
+
+ +
+ + STOPPED +
+ +
-
Total Accounts
0
-
Total Points
0
-
Completed
0
-
Errors
0
-
-
-

🎮 Control Panel

-
- - - - - +
+
+
+
Total Accounts
+
0
+
+
+
+
+
+
Total Points
+
0
+
+
+
+
+
+
Completed Today
+
0
+
+
+
+
+
+
Errors
+
0
+
+
+
+
+
+
Uptime
+
00:00:00
+
+
+
+
+
+
Memory
+
0 MB
+
-
-

👥 Accounts

-
+ + +
+ +
+ +
+
+

Control Panel

+
+ +
+
+
+ + + + +
+
+ + +
+
+

Accounts

+ 0 +
+
+
+ + +
+
+

Quick Actions

+
+
+ + + + +
+
+
+ + +
+ +
+
+

Points History

+
+ + +
+
+
+ +
+
+ + +
+
+

Activity Breakdown

+
+
+ +
+
+
+ +
-

📝 Live Logs

+
+

Live Logs

+
+ + + +
+
+ + +
+ +
+ + + + - + + \ No newline at end of file diff --git a/public/style.css b/public/style.css index e98b7fd..6295866 100644 --- a/public/style.css +++ b/public/style.css @@ -1,3 +1,68 @@ +/* Microsoft Rewards Bot Dashboard - CSS */ + +:root { + /* Colors - Dark Theme */ + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #21262d; + --bg-card: #161b22; + --border-color: #30363d; + --text-primary: #f0f6fc; + --text-secondary: #8b949e; + --text-muted: #6e7681; + + /* Accent Colors */ + --accent-blue: #58a6ff; + --accent-green: #3fb950; + --accent-red: #f85149; + --accent-orange: #d29922; + --accent-purple: #a371f7; + --accent-cyan: #39c5cf; + + /* Status Colors */ + --status-running: var(--accent-green); + --status-stopped: var(--text-muted); + --status-error: var(--accent-red); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.5); + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 9999px; +} + +/* Light Theme */ +.light-theme { + --bg-primary: #ffffff; + --bg-secondary: #f6f8fa; + --bg-tertiary: #eaeef2; + --bg-card: #ffffff; + --border-color: #d0d7de; + --text-primary: #1f2328; + --text-secondary: #656d76; + --text-muted: #8c959f; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.15); + --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.2); +} + +/* Reset & Base */ * { margin: 0; padding: 0; @@ -5,520 +70,811 @@ } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: #0d1117; - color: #e6edf3; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; min-height: 100vh; - padding: 20px; } +/* Container */ .container { - max-width: 1400px; + max-width: 1600px; margin: 0 auto; + padding: var(--spacing-lg); } /* Header */ .header { - background: #161b22; - padding: 24px; - border-radius: 12px; - margin-bottom: 20px; - border: 1px solid #30363d; display: flex; justify-content: space-between; align-items: center; + margin-bottom: var(--spacing-xl); + padding-bottom: var(--spacing-lg); + border-bottom: 1px solid var(--border-color); +} + +.header-left { + display: flex; + align-items: center; + gap: var(--spacing-md); } .header h1 { - color: #58a6ff; - font-size: 28px; + font-size: 1.75rem; font-weight: 600; + background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } -.header .status { +.version { + background: var(--bg-tertiary); + color: var(--text-secondary); + padding: 0.25rem 0.75rem; + border-radius: var(--radius-full); + font-size: 0.75rem; + font-weight: 500; +} + +.header-right { display: flex; align-items: center; - gap: 12px; + gap: var(--spacing-md); } +.theme-toggle { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-secondary); + width: 40px; + height: 40px; + border-radius: var(--radius-md); + cursor: pointer; + transition: var(--transition-fast); +} + +.theme-toggle:hover { + color: var(--text-primary); + background: var(--bg-secondary); +} + +/* Status Badge */ .status-badge { - padding: 8px 16px; - border-radius: 8px; - font-size: 13px; + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: 0.5rem 1rem; + border-radius: var(--radius-full); + font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } +.status-badge i { + font-size: 0.625rem; +} + .status-running { - background: #238636; - color: white; + background: rgba(63, 185, 80, 0.15); + color: var(--accent-green); +} + +.status-running i { + animation: pulse 1.5s ease-in-out infinite; } .status-stopped { - background: #da3633; - color: white; + background: rgba(110, 118, 129, 0.15); + color: var(--text-muted); } -.theme-toggle { - background: #21262d; - border: 1px solid #30363d; - color: #e6edf3; - padding: 8px 12px; - border-radius: 8px; - cursor: pointer; - font-size: 18px; +.status-error { + background: rgba(248, 81, 73, 0.15); + color: var(--accent-red); } -.theme-toggle:hover { - background: #30363d; +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.4; + } } /* Stats Grid */ .stats-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 16px; - margin-bottom: 20px; + grid-template-columns: repeat(6, 1fr); + gap: var(--spacing-md); + margin-bottom: var(--spacing-xl); } .stat-card { - background: #161b22; - border: 1px solid #30363d; - border-radius: 12px; - padding: 20px; - transition: all 0.2s; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + display: flex; + align-items: center; + gap: var(--spacing-md); + transition: var(--transition-normal); } .stat-card:hover { - border-color: #58a6ff; transform: translateY(-2px); + box-shadow: var(--shadow-md); + border-color: var(--accent-blue); } -.stat-card .label { - color: #8b949e; - font-size: 13px; - font-weight: 600; +.stat-icon { + width: 48px; + height: 48px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + background: var(--bg-tertiary); + color: var(--accent-blue); +} + +.stat-points .stat-icon { + color: var(--accent-orange); + background: rgba(210, 153, 34, 0.15); +} + +.stat-success .stat-icon { + color: var(--accent-green); + background: rgba(63, 185, 80, 0.15); +} + +.stat-error .stat-icon { + color: var(--accent-red); + background: rgba(248, 81, 73, 0.15); +} + +.stat-content { + flex: 1; +} + +.stat-label { + font-size: 0.75rem; + color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; - margin-bottom: 8px; + margin-bottom: 0.25rem; } -.stat-card .value { - color: #e6edf3; - font-size: 32px; +.stat-value { + font-size: 1.5rem; font-weight: 700; + color: var(--text-primary); } -/* Card */ +/* Main Grid */ +.main-grid { + display: grid; + grid-template-columns: 380px 1fr; + gap: var(--spacing-lg); + margin-bottom: var(--spacing-lg); +} + +.left-column, +.right-column { + display: flex; + flex-direction: column; + gap: var(--spacing-lg); +} + +/* Cards */ .card { - background: #161b22; - border: 1px solid #30363d; - border-radius: 12px; - padding: 24px; - margin-bottom: 20px; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + overflow: hidden; } .card-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; - padding-bottom: 16px; - border-bottom: 1px solid #30363d; + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 1px solid var(--border-color); + background: var(--bg-secondary); } .card-title { - color: #e6edf3; - font-size: 18px; + font-size: 0.875rem; font-weight: 600; + color: var(--text-primary); + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.card-title i { + color: var(--accent-blue); +} + +.card-actions { + display: flex; + align-items: center; + gap: var(--spacing-sm); } /* Buttons */ -.btn-group { - display: flex; - gap: 12px; - flex-wrap: wrap; -} - .btn { - padding: 10px 20px; - border: none; - border-radius: 8px; - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; display: inline-flex; align-items: center; - gap: 8px; + justify-content: center; + gap: var(--spacing-sm); + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition-fast); + border: none; } -.btn:hover { - transform: translateY(-2px); -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none !important; -} - -.btn-success { - background: #238636; - color: white; -} - -.btn-success:hover { - background: #2ea043; -} - -.btn-danger { - background: #da3633; - color: white; -} - -.btn-danger:hover { - background: #f85149; +.btn-sm { + padding: 0.375rem 0.75rem; + font-size: 0.75rem; } .btn-primary { - background: #1f6feb; + background: var(--accent-blue); color: white; } .btn-primary:hover { - background: #58a6ff; + background: #4c9ced; +} + +.btn-secondary { + background: var(--bg-tertiary); + color: var(--text-secondary); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + color: var(--text-primary); + background: var(--bg-secondary); +} + +.btn-danger { + background: var(--accent-red); + color: white; +} + +.btn-danger:hover { + background: #e64a42; +} + +/* Control Grid */ +.control-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--spacing-md); + padding: var(--spacing-lg); +} + +.control-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + padding: var(--spacing-lg); + border-radius: var(--radius-md); + border: 2px solid transparent; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: var(--transition-normal); + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.control-btn i { + font-size: 1.5rem; +} + +.control-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.control-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.control-start:hover:not(:disabled) { + border-color: var(--accent-green); + color: var(--accent-green); +} + +.control-stop:hover:not(:disabled) { + border-color: var(--accent-red); + color: var(--accent-red); +} + +.control-restart:hover:not(:disabled) { + border-color: var(--accent-orange); + color: var(--accent-orange); +} + +.control-reset:hover:not(:disabled) { + border-color: var(--accent-purple); + color: var(--accent-purple); +} + +/* Accounts List */ +.accounts-list { + max-height: 300px; + overflow-y: auto; } -/* Accounts */ .account-item { display: flex; - justify-content: space-between; align-items: center; - padding: 16px; - background: #0d1117; - border: 1px solid #30363d; - border-radius: 8px; - margin-bottom: 12px; - transition: all 0.2s; + justify-content: space-between; + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 1px solid var(--border-color); + transition: var(--transition-fast); +} + +.account-item:last-child { + border-bottom: none; } .account-item:hover { - border-color: #58a6ff; - transform: translateX(4px); + background: var(--bg-tertiary); } .account-info { display: flex; align-items: center; - gap: 12px; + gap: var(--spacing-md); } .account-avatar { - width: 40px; - height: 40px; - border-radius: 8px; - background: linear-gradient(135deg, #1f6feb, #a371f7); + width: 36px; + height: 36px; + border-radius: var(--radius-full); + background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); display: flex; align-items: center; justify-content: center; + font-weight: 600; + font-size: 0.875rem; color: white; - font-weight: 700; - font-size: 16px; -} - -.account-details { - flex: 1; } .account-email { - font-weight: 600; - color: #e6edf3; - margin-bottom: 4px; + font-size: 0.875rem; + color: var(--text-primary); } -.account-status-text { - font-size: 12px; - color: #8b949e; +.account-status { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + border-radius: var(--radius-full); } -.account-stats { +.account-status.completed { + background: rgba(63, 185, 80, 0.15); + color: var(--accent-green); +} + +.account-status.pending { + background: rgba(210, 153, 34, 0.15); + color: var(--accent-orange); +} + +.account-status.error { + background: rgba(248, 81, 73, 0.15); + color: var(--accent-red); +} + +/* Quick Actions */ +.quick-actions { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--spacing-sm); + padding: var(--spacing-md); +} + +.action-btn { display: flex; + flex-direction: column; align-items: center; - gap: 24px; + gap: var(--spacing-xs); + padding: var(--spacing-md); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + color: var(--text-secondary); + font-size: 0.75rem; + cursor: pointer; + transition: var(--transition-fast); } -.account-points { - text-align: right; +.action-btn i { + font-size: 1rem; + color: var(--accent-blue); } -.account-points-value { - font-size: 24px; - font-weight: 700; - color: #58a6ff; +.action-btn:hover { + background: var(--bg-secondary); + color: var(--text-primary); + border-color: var(--accent-blue); } -.account-points-label { - font-size: 11px; - color: #8b949e; - text-transform: uppercase; - letter-spacing: 0.5px; +/* Charts */ +.chart-container { + padding: var(--spacing-lg); + height: 250px; } -.account-badge { - padding: 6px 12px; - border-radius: 6px; - font-size: 11px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.5px; +.chart-small { + height: 200px; } -.badge-idle { - background: #21262d; - color: #8b949e; +.chart-period { + display: flex; + gap: var(--spacing-xs); } -.badge-running { - background: rgba(35, 134, 54, 0.2); - color: #3fb950; - border: 1px solid #3fb950; +.period-btn { + padding: 0.25rem 0.75rem; + border-radius: var(--radius-full); + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-secondary); + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition-fast); } -.badge-completed { - background: rgba(31, 111, 235, 0.2); - color: #58a6ff; - border: 1px solid #58a6ff; +.period-btn:hover { + border-color: var(--accent-blue); + color: var(--text-primary); } -.badge-error { - background: rgba(248, 81, 73, 0.2); - color: #f85149; - border: 1px solid #f85149; +.period-btn.active { + background: var(--accent-blue); + border-color: var(--accent-blue); + color: white; } -/* Logs */ +/* Badge */ +.badge { + background: var(--accent-blue); + color: white; + padding: 0.125rem 0.5rem; + border-radius: var(--radius-full); + font-size: 0.75rem; + font-weight: 600; +} + +/* Log Filter */ +.log-filter { + padding: 0.375rem 0.75rem; + border-radius: var(--radius-md); + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-primary); + font-size: 0.75rem; + cursor: pointer; +} + +/* Logs Container */ .logs-container { - background: #0d1117; - border: 1px solid #30363d; - border-radius: 8px; - padding: 16px; - max-height: 600px; + height: 350px; overflow-y: auto; - font-family: 'Consolas', 'Monaco', monospace; - font-size: 13px; - line-height: 1.6; -} - -.logs-container::-webkit-scrollbar { - width: 10px; -} - -.logs-container::-webkit-scrollbar-track { - background: #161b22; - border-radius: 5px; -} - -.logs-container::-webkit-scrollbar-thumb { - background: #30363d; - border-radius: 5px; -} - -.logs-container::-webkit-scrollbar-thumb:hover { - background: #484f58; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 0.8125rem; + background: var(--bg-primary); + padding: var(--spacing-md); } .log-entry { - padding: 8px; - margin-bottom: 4px; - border-left: 2px solid transparent; - border-radius: 4px; - transition: all 0.15s; + display: flex; + padding: 0.375rem 0; + border-bottom: 1px solid var(--border-color); + animation: fadeIn 0.2s ease; } -.log-entry:hover { - background: #161b22; +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-5px); + } + + to { + opacity: 1; + transform: translateY(0); + } } -.log-timestamp { - color: #8b949e; - margin-right: 8px; +.log-time { + color: var(--text-muted); + min-width: 85px; + flex-shrink: 0; } -.log-platform { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - font-size: 11px; - font-weight: 700; - margin-right: 8px; +.log-level { + min-width: 60px; + flex-shrink: 0; + font-weight: 600; text-transform: uppercase; } -.platform-MAIN { - background: rgba(31, 111, 235, 0.2); - color: #58a6ff; +.log-level.error { + color: var(--accent-red); } -.platform-DESKTOP { - background: rgba(163, 113, 247, 0.2); - color: #a371f7; +.log-level.warn { + color: var(--accent-orange); } -.platform-MOBILE { - background: rgba(63, 185, 80, 0.2); - color: #3fb950; +.log-level.log { + color: var(--accent-blue); } -.log-title { - color: #8b949e; - margin-right: 8px; - font-weight: 700; +.log-level.info { + color: var(--accent-cyan); } -.log-level-log { - border-left-color: #3fb950; - color: #e6edf3; +.log-source { + color: var(--accent-purple); + min-width: 100px; + flex-shrink: 0; } -.log-level-warn { - border-left-color: #d29922; - color: #d29922; +.log-message { + color: var(--text-primary); + word-break: break-word; + flex: 1; } -.log-level-error { - border-left-color: #f85149; - color: #f85149; - font-weight: 600; -} - -/* Empty State */ -.empty-state { +.log-empty { + color: var(--text-muted); text-align: center; - padding: 60px 20px; - color: #8b949e; + padding: var(--spacing-xl); } -.empty-state i { - font-size: 48px; - margin-bottom: 16px; - opacity: 0.3; +/* Footer */ +.footer { + text-align: center; + padding: var(--spacing-lg) 0; + color: var(--text-muted); + font-size: 0.75rem; + border-top: 1px solid var(--border-color); + margin-top: var(--spacing-lg); } -/* Loading */ -.loading { - display: flex; +.footer a { + color: var(--accent-blue); + text-decoration: none; +} + +.footer a:hover { + text-decoration: underline; +} + +.separator { + margin: 0 var(--spacing-sm); +} + +.connection-status { + display: inline-flex; align-items: center; - justify-content: center; - padding: 60px; - gap: 16px; + gap: var(--spacing-xs); } -.spinner { - width: 40px; - height: 40px; - border: 3px solid #30363d; - border-top-color: #58a6ff; - border-radius: 50%; - animation: spin 0.8s linear infinite; +.connection-status i { + font-size: 0.5rem; } -@keyframes spin { - to { transform: rotate(360deg); } +.connection-status.connected { + color: var(--accent-green); +} + +.connection-status.disconnected { + color: var(--accent-red); } /* Toast */ -.toast { +#toastContainer { position: fixed; - bottom: 24px; - right: 24px; - background: #161b22; - border: 1px solid #30363d; - padding: 16px 20px; - border-radius: 8px; + bottom: var(--spacing-lg); + right: var(--spacing-lg); + z-index: 1000; + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.toast { display: flex; align-items: center; - gap: 12px; - min-width: 300px; - z-index: 1000; + gap: var(--spacing-md); + padding: var(--spacing-md) var(--spacing-lg); + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); animation: slideIn 0.3s ease; + max-width: 350px; +} + +.toast.success { + border-left: 4px solid var(--accent-green); +} + +.toast.error { + border-left: 4px solid var(--accent-red); +} + +.toast.warning { + border-left: 4px solid var(--accent-orange); +} + +.toast.info { + border-left: 4px solid var(--accent-blue); } @keyframes slideIn { from { + transform: translateX(100%); opacity: 0; - transform: translateX(100px); } + to { - opacity: 1; transform: translateX(0); + opacity: 1; } } -.toast i { - font-size: 20px; +/* Modal */ +.modal { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 1001; + justify-content: center; + align-items: center; + backdrop-filter: blur(4px); } -.toast-success { - border-left: 3px solid #3fb950; +.modal.show { + display: flex; } -.toast-success i { - color: #3fb950; +.modal-content { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow: hidden; + animation: modalIn 0.3s ease; } -.toast-error { - border-left: 3px solid #f85149; +@keyframes modalIn { + from { + transform: scale(0.9); + opacity: 0; + } + + to { + transform: scale(1); + opacity: 1; + } } -.toast-error i { - color: #f85149; +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-lg); + border-bottom: 1px solid var(--border-color); + background: var(--bg-secondary); } -.toast-info { - border-left: 3px solid #58a6ff; +.modal-header h3 { + font-size: 1rem; + font-weight: 600; } -.toast-info i { - color: #58a6ff; +.modal-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.5rem; + cursor: pointer; + padding: 0; + line-height: 1; } -/* Light Theme */ -body.light-theme { - background: #f6f8fa; - color: #24292f; +.modal-close:hover { + color: var(--text-primary); } -body.light-theme .header, -body.light-theme .stat-card, -body.light-theme .card { - background: #ffffff; - border-color: #d0d7de; +.modal-body { + padding: var(--spacing-lg); + overflow-y: auto; + max-height: 60vh; } -body.light-theme .card-title, -body.light-theme .stat-card .value, -body.light-theme .account-email { - color: #24292f; +.modal-footer { + display: flex; + justify-content: flex-end; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-lg); + border-top: 1px solid var(--border-color); + background: var(--bg-secondary); } -body.light-theme .theme-toggle, -body.light-theme .account-item { - background: #f6f8fa; - border-color: #d0d7de; +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; } -body.light-theme .logs-container { - background: #ffffff; - border-color: #d0d7de; +::-webkit-scrollbar-track { + background: var(--bg-primary); } -body.light-theme .log-entry:hover { - background: #f6f8fa; +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: var(--radius-full); } -body.light-theme .toast { - background: #ffffff; - border-color: #d0d7de; +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); } + +/* Responsive */ +@media (max-width: 1200px) { + .stats-grid { + grid-template-columns: repeat(3, 1fr); + } + + .main-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .container { + padding: var(--spacing-md); + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .header { + flex-direction: column; + gap: var(--spacing-md); + text-align: center; + } + + .control-grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index a00c931..e350ef7 100644 --- a/src/browser/Browser.ts +++ b/src/browser/Browser.ts @@ -257,11 +257,162 @@ class Browser { description: 'Portable Document Format', filename: 'internal-pdf-viewer', length: 2 + }, + { + name: 'Chromium PDF Viewer', + description: 'Portable Document Format', + filename: 'internal-pdf-viewer', + length: 2 } ] }) } catch { /* Plugins may be frozen */ } + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 5: WebRTC Leak Prevention + // ═══════════════════════════════════════════════════════════════ + + // CRITICAL: Prevent WebRTC from leaking real IP address + try { + // Override RTCPeerConnection to prevent IP leaks + const originalRTCPeerConnection = window.RTCPeerConnection + // @ts-ignore + window.RTCPeerConnection = function (config?: RTCConfiguration) { + // Force STUN servers through proxy or disable + const modifiedConfig: RTCConfiguration = { + ...config, + iceServers: [] // Disable ICE to prevent IP leak + } + return new originalRTCPeerConnection(modifiedConfig) + } + // @ts-ignore + window.RTCPeerConnection.prototype = originalRTCPeerConnection.prototype + } catch { /* WebRTC override may fail */ } + + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 6: Battery API Spoofing + // ═══════════════════════════════════════════════════════════════ + + // Headless browsers may have unusual battery states + try { + // @ts-ignore + if (navigator.getBattery) { + // @ts-ignore + navigator.getBattery = () => Promise.resolve({ + charging: true, + chargingTime: 0, + dischargingTime: Infinity, + level: 1, + addEventListener: () => { }, + removeEventListener: () => { }, + dispatchEvent: () => true + }) + } + } catch { /* Battery API override may fail */ } + + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 7: Hardware Concurrency Consistency + // ═══════════════════════════════════════════════════════════════ + + // Ensure hardware concurrency looks realistic + try { + const realCores = navigator.hardwareConcurrency || 4 + // Round to common values: 2, 4, 6, 8, 12, 16 + const commonCores = [2, 4, 6, 8, 12, 16] + const normalizedCores = commonCores.reduce((prev, curr) => + Math.abs(curr - realCores) < Math.abs(prev - realCores) ? curr : prev + ) + Object.defineProperty(navigator, 'hardwareConcurrency', { + get: () => normalizedCores, + configurable: true + }) + } catch { /* Hardware concurrency override may fail */ } + + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 8: Device Memory Consistency + // ═══════════════════════════════════════════════════════════════ + + try { + // @ts-ignore + const realMemory = navigator.deviceMemory || 8 + // Round to common values: 2, 4, 8, 16 + const commonMemory = [2, 4, 8, 16] + const normalizedMemory = commonMemory.reduce((prev, curr) => + Math.abs(curr - realMemory) < Math.abs(prev - realMemory) ? curr : prev + ) + Object.defineProperty(navigator, 'deviceMemory', { + get: () => normalizedMemory, + configurable: true + }) + } catch { /* Device memory override may fail */ } + + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 9: Audio Fingerprint Protection + // ═══════════════════════════════════════════════════════════════ + + try { + const originalCreateOscillator = AudioContext.prototype.createOscillator + const originalCreateDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor + + // Add slight randomization to audio context to prevent fingerprinting + AudioContext.prototype.createOscillator = function () { + const oscillator = originalCreateOscillator.apply(this) + const originalGetFloatFrequencyData = AnalyserNode.prototype.getFloatFrequencyData + AnalyserNode.prototype.getFloatFrequencyData = function (array) { + originalGetFloatFrequencyData.apply(this, [array]) + // Add imperceptible noise + for (let i = 0; i < array.length; i++) { + array[i] = array[i]! + (Math.random() * 0.0001) + } + } + return oscillator + } + + AudioContext.prototype.createDynamicsCompressor = function () { + const compressor = originalCreateDynamicsCompressor.apply(this) + // Slightly randomize default values + try { + compressor.threshold.value = -24 + (Math.random() * 0.001) + compressor.knee.value = 30 + (Math.random() * 0.001) + } catch { /* May be read-only */ } + return compressor + } + } catch { /* Audio API override may fail */ } + + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 10: Timezone & Locale Consistency + // ═══════════════════════════════════════════════════════════════ + + try { + // Ensure Date.prototype.getTimezoneOffset is consistent + const originalGetTimezoneOffset = Date.prototype.getTimezoneOffset + const consistentOffset = originalGetTimezoneOffset.call(new Date()) + Date.prototype.getTimezoneOffset = function () { + return consistentOffset + } + } catch { /* Timezone override may fail */ } + + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 11: Connection Info Spoofing + // ═══════════════════════════════════════════════════════════════ + + try { + // @ts-ignore + if (navigator.connection) { + Object.defineProperty(navigator, 'connection', { + get: () => ({ + effectiveType: '4g', + rtt: 50, + downlink: 10, + saveData: false, + addEventListener: () => { }, + removeEventListener: () => { } + }), + configurable: true + }) + } + } catch { /* Connection API override may fail */ } + // ═══════════════════════════════════════════════════════════════ // Standard styling (non-detection related) // ═══════════════════════════════════════════════════════════════ diff --git a/src/dashboard/README.md b/src/dashboard/README.md deleted file mode 100644 index 8b37f00..0000000 --- a/src/dashboard/README.md +++ /dev/null @@ -1,305 +0,0 @@ -# Dashboard - Modern Real-Time Interface - -## 🎨 New Features (2025 Update - November) - -### ✨ Modern UI Enhancements v2.0 -- **Professional Dark Theme**: Default dark mode with improved color palette and contrast -- **Refined Design System**: Consistent spacing, typography, and component styling -- **Improved Animations**: Smoother transitions with optimized performance -- **Enhanced Glassmorphism**: Better backdrop blur and shadow layering -- **Staggered Card Entrance**: Beautiful loading animations for stats cards -- **Better Visual Hierarchy**: Improved text sizing and weight differentiation -- **Refined Components**: Polished buttons, badges, and interactive elements -- **Optimized Icons**: Gradient overlays with better sizing - -### Previous v1.0 Features -- **Dark Mode Support**: Toggle between light and dark themes with persistent preference -- **Real-Time Updates**: WebSocket-powered live log streaming and status updates -- **Glassmorphism Design**: Modern blur effects and smooth animations -- **Responsive Layout**: Optimized for desktop, tablet, and mobile devices -- **Enhanced Stats Cards**: Animated counters with gradient icons -- **Log Statistics**: Real-time error and warning counters - -### 🚀 Performance Improvements -- **Optimized Log Management**: Maximum 200 logs in memory with automatic cleanup -- **Smart WebSocket Reconnection**: Automatic reconnection on network failures -- **Reduced Bundle Size**: Removed unused console.log calls (-15% size) -- **Better Error Handling**: Comprehensive validation and user-friendly error messages - -### 🔧 Technical Enhancements -- **Proper Log Interception**: Fixed "No logs yet..." issue by intercepting at module level -- **Type-Safe API**: Full TypeScript support with proper error handling -- **Consistent Logging**: All console.log calls replaced with structured logging -- **Memory Management**: Automatic cleanup of old WebSocket buffers - ---- - -## 📊 Dashboard UI - -### Control Panel -- **Start/Stop/Restart Bot**: Full bot lifecycle management -- **Refresh Data**: Manual data synchronization -- **Clear Logs**: Reset log history - -### Real-Time Monitoring -- **Live Logs**: Color-coded logs with timestamps, platform tags, and titles -- **Account Status**: Per-account progress with points tracking -- **Statistics Dashboard**: Total accounts, points, completed runs, errors - -### Theme Support -- **Light Mode**: Clean white interface with subtle shadows -- **Dark Mode**: Eye-friendly dark interface for night work - ---- - -## API Endpoints - -### Status & Control - -#### `GET /api/status` -Get current bot status. - -**Response:** -```json -{ - "running": false, - "lastRun": "2025-11-03T10:30:00.000Z", - "currentAccount": "user@example.com", - "totalAccounts": 5 -} -``` - -#### `POST /api/start` -Start bot execution in background. - -**Response:** -```json -{ - "success": true, - "pid": 12345 -} -``` - -#### `POST /api/stop` -Stop bot execution. - -**Response:** -```json -{ - "success": true -} -``` - ---- - -### Accounts - -#### `GET /api/accounts` -List all accounts with masked emails and status. - -**Response:** -```json -[ - { - "email": "user@example.com", - "maskedEmail": "u***@e***.com", - "points": 5420, - "lastSync": "2025-11-03T10:30:00.000Z", - "status": "completed", - "errors": [] - } -] -``` - -#### `POST /api/sync/:email` -Force synchronization for a single account. - -**Parameters:** -- `email` (path): Account email - -**Response:** -```json -{ - "success": true, - "pid": 12346 -} -``` - ---- - -### Logs & History - -#### `GET /api/logs?limit=100` -Get recent logs. - -**Query Parameters:** -- `limit` (optional): Max number of logs (default: 100, max: 500) - -**Response:** -```json -[ - { - "timestamp": "2025-11-03T10:30:00.000Z", - "level": "log", - "platform": "DESKTOP", - "title": "SEARCH", - "message": "Completed 30 searches" - } -] -``` - -#### `DELETE /api/logs` -Clear all logs from memory. - -**Response:** -```json -{ - "success": true -} -``` - -#### `GET /api/history` -Get recent run summaries (last 7 days). - -**Response:** -```json -[ - { - "runId": "abc123", - "timestamp": "2025-11-03T10:00:00.000Z", - "totals": { - "totalCollected": 450, - "totalAccounts": 5, - "accountsWithErrors": 0 - }, - "perAccount": [...] - } -] -``` - ---- - -### Configuration - -#### `GET /api/config` -Get current configuration (sensitive data masked). - -**Response:** -```json -{ - "baseURL": "https://rewards.bing.com", - "headless": true, - "clusters": 2, - "webhook": { - "enabled": true, - "url": "htt***://dis***" - } -} -``` - -#### `POST /api/config` -Update configuration (creates automatic backup). - -**Request Body:** Full config object - -**Response:** -```json -{ - "success": true, - "backup": "/path/to/config.jsonc.backup.1730634000000" -} -``` - ---- - -### Metrics - -#### `GET /api/metrics` -Get aggregated metrics. - -**Response:** -```json -{ - "totalAccounts": 5, - "totalPoints": 27100, - "accountsWithErrors": 0, - "accountsRunning": 0, - "accountsCompleted": 5 -} -``` - ---- - -## WebSocket - -Connect to `ws://localhost:3000/ws` for real-time log streaming. - -**Message Format:** -```json -{ - "type": "log", - "log": { - "timestamp": "2025-11-03T10:30:00.000Z", - "level": "log", - "platform": "DESKTOP", - "title": "SEARCH", - "message": "Completed search" - } -} -``` - -**On Connect:** -Receives history of last 50 logs: -```json -{ - "type": "history", - "logs": [...] -} -``` - ---- - -## Usage - -### Start Dashboard -```bash -npm run dashboard -# or in dev mode -npm run dashboard-dev -``` - -Default: `http://127.0.0.1:3000` - -### Environment Variables -- `DASHBOARD_PORT`: Port number (default: 3000) -- `DASHBOARD_HOST`: Bind address (default: 127.0.0.1) - -### Security -- **Localhost only**: Dashboard binds to `127.0.0.1` by default -- **Email masking**: Emails are partially masked in API responses -- **Token masking**: Webhook URLs and auth tokens are masked -- **Config backup**: Automatic backup before any config modification - ---- - -## Example Usage - -### Check Status -```bash -curl http://localhost:3000/api/status -``` - -### Start Bot -```bash -curl -X POST http://localhost:3000/api/start -``` - -### Get Logs -```bash -curl http://localhost:3000/api/logs?limit=50 -``` - -### Sync Single Account -```bash -curl -X POST http://localhost:3000/api/sync/user@example.com -``` diff --git a/src/dashboard/routes.ts b/src/dashboard/routes.ts index ace7d6a..f0b068f 100644 --- a/src/dashboard/routes.ts +++ b/src/dashboard/routes.ts @@ -259,6 +259,78 @@ apiRouter.post('/account/:email/reset', (req: Request, res: Response): void => { } }) +// POST /api/reset-state - Reset all job states for today +apiRouter.post('/reset-state', (_req: Request, res: Response): void => { + try { + const jobStatePath = path.join(process.cwd(), 'sessions', 'job-state') + + if (!fs.existsSync(jobStatePath)) { + res.json({ success: true, message: 'No job state to reset' }) + return + } + + const today = new Date().toISOString().slice(0, 10) + let resetCount = 0 + + // Read all job state files and reset today's entries + const files = fs.readdirSync(jobStatePath).filter(f => f.endsWith('.json')) + + for (const file of files) { + try { + const filePath = path.join(jobStatePath, file) + const content = JSON.parse(fs.readFileSync(filePath, 'utf-8')) + + // Reset today's completed activities + if (content[today]) { + delete content[today] + fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf-8') + resetCount++ + } + } catch { + // Continue processing other files if one fails + continue + } + } + + // Reset account statuses in dashboard state + const accounts = dashboardState.getAccounts() + for (const account of accounts) { + dashboardState.updateAccount(account.email, { + status: 'idle', + errors: [] + }) + } + + res.json({ + success: true, + message: `Reset job state for ${resetCount} account(s)`, + resetCount + }) + } catch (error) { + res.status(500).json({ error: getErr(error) }) + } +}) + +// GET /api/memory - Get current memory usage +apiRouter.get('/memory', (_req: Request, res: Response) => { + try { + const memUsage = process.memoryUsage() + res.json({ + heapUsed: memUsage.heapUsed, + heapTotal: memUsage.heapTotal, + rss: memUsage.rss, + external: memUsage.external, + formatted: { + heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(1)} MB`, + heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(1)} MB`, + rss: `${(memUsage.rss / 1024 / 1024).toFixed(1)} MB` + } + }) + } catch (error) { + res.status(500).json({ error: getErr(error) }) + } +}) + // Helper to mask sensitive URLs function maskUrl(url: string): string { try {