mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 17:26:17 +00:00
1. Fixed Dashboard Issues Resolved: ❌ Unexpected token 'class' - File corrupted by a formatter → Cleanly rewritten ❌ startBot is not defined - Broken template literals → Code rewritten to ES5-compatible ❌ Tracking Prevention blocked access - External CDNs blocked → SVG icons inline (except Chart.js) ❌ favicon.ico 404 - No favicon → Added an inline emoji favicon Modified Files: app.js - Rewritten without problematic template literals index.html - SVG icons inline, favicon inline 2. Enhanced Anti-Detection Browser.ts - Expanded Chromium arguments: --disable-automation - Hides automation flag --force-webrtc-ip-handling-policy=disable_non_proxied_udp - Blocks WebRTC IP leaks --disable-webrtc-hw-encoding/decoding - Disables WebRTC hardware encoding --disable-gpu-sandbox, --disable-accelerated-2d-canvas - Reduces GPU fingerprinting --disable-client-side-phishing-detection - Disables anti-phishing detection --disable-features=TranslateUI,site-per-process,IsolateOrigins - Hides bot-like features --no-zygote, --single-process - Single-process mode (harder to detect) --enable-features=NetworkService,NetworkServiceInProcess - Integrated network 11 layers of JavaScript anti-detection (existing, verified): navigator.webdriver Removed window.chrome.runtime spoofed Canvas fingerprint randomized WebGL renderer hidden API permissions normalized Realistic plugins injected WebRTC IP leak prevention Battery API spoofed Hardware concurrency normalized Audio fingerprint protected Connection info spoofed 3. Dashboard - Features ✅ Real-time WebSocket for logs ✅ Chart.js graphs (Points History, Activity Breakdown) ✅ Start/Stop/Restart/Reset State controls ✅ Account list with status ✅ Light/dark theme with persistence ✅ Log export to text file ✅ Toast notifications ✅ Modal system
703 lines
21 KiB
JavaScript
703 lines
21 KiB
JavaScript
/**
|
|
* Microsoft Rewards Bot Dashboard - Frontend JavaScript
|
|
*/
|
|
|
|
// State
|
|
const state = {
|
|
isRunning: false,
|
|
autoScroll: true,
|
|
logs: [],
|
|
accounts: [],
|
|
stats: { totalAccounts: 0, totalPoints: 0, completed: 0, errors: 0, startTime: null },
|
|
currentLogFilter: 'all',
|
|
ws: null,
|
|
reconnectAttempts: 0
|
|
}
|
|
|
|
let pointsChart = null
|
|
let activityChart = null
|
|
|
|
// Initialize on DOM ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initWebSocket()
|
|
initCharts()
|
|
loadInitialData()
|
|
startUptimeTimer()
|
|
loadTheme()
|
|
})
|
|
|
|
// WebSocket
|
|
function initWebSocket() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
const wsUrl = protocol + '//' + window.location.host
|
|
|
|
try {
|
|
state.ws = new WebSocket(wsUrl)
|
|
|
|
state.ws.onopen = function () {
|
|
updateConnectionStatus(true)
|
|
state.reconnectAttempts = 0
|
|
console.log('[WS] Connected')
|
|
}
|
|
|
|
state.ws.onmessage = function (event) {
|
|
try {
|
|
handleWsMessage(JSON.parse(event.data))
|
|
} catch (e) {
|
|
console.error('[WS] Parse error:', e)
|
|
}
|
|
}
|
|
|
|
state.ws.onclose = function () {
|
|
updateConnectionStatus(false)
|
|
attemptReconnect()
|
|
}
|
|
|
|
state.ws.onerror = function (e) {
|
|
console.error('[WS] Error:', e)
|
|
}
|
|
} catch (e) {
|
|
console.error('[WS] Failed:', e)
|
|
attemptReconnect()
|
|
}
|
|
}
|
|
|
|
function attemptReconnect() {
|
|
if (state.reconnectAttempts >= 10) {
|
|
showToast('Connection lost. Please refresh.', 'error')
|
|
return
|
|
}
|
|
state.reconnectAttempts++
|
|
setTimeout(initWebSocket, Math.min(1000 * Math.pow(1.5, state.reconnectAttempts), 30000))
|
|
}
|
|
|
|
function handleWsMessage(data) {
|
|
if (data.type === 'init' && data.data) {
|
|
if (data.data.logs) data.data.logs.forEach(addLogEntry)
|
|
if (data.data.status) updateBotStatus(data.data.status)
|
|
if (data.data.accounts) renderAccounts(data.data.accounts)
|
|
return
|
|
}
|
|
|
|
const payload = data.payload || data.data || data
|
|
|
|
switch (data.type) {
|
|
case 'log':
|
|
addLogEntry(payload.log || payload)
|
|
break
|
|
case 'status':
|
|
updateBotStatus(payload)
|
|
break
|
|
case 'stats':
|
|
updateStats(payload)
|
|
break
|
|
case 'account':
|
|
case 'account_update':
|
|
updateAccountStatus(payload)
|
|
break
|
|
case 'accounts':
|
|
renderAccounts(payload)
|
|
break
|
|
}
|
|
}
|
|
|
|
function updateConnectionStatus(connected) {
|
|
const el = document.getElementById('connectionStatus')
|
|
if (el) {
|
|
el.className = 'connection-status ' + (connected ? 'connected' : 'disconnected')
|
|
el.innerHTML = '<i class="fas fa-circle"></i> ' + (connected ? 'Connected' : 'Disconnected')
|
|
}
|
|
}
|
|
|
|
// Charts
|
|
function initCharts() {
|
|
if (typeof Chart === 'undefined') {
|
|
console.warn('Chart.js not loaded')
|
|
return
|
|
}
|
|
initPointsChart()
|
|
initActivityChart()
|
|
}
|
|
|
|
function initPointsChart() {
|
|
const ctx = document.getElementById('pointsChart')
|
|
if (!ctx) return
|
|
|
|
const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 250)
|
|
gradient.addColorStop(0, 'rgba(88, 166, 255, 0.3)')
|
|
gradient.addColorStop(1, 'rgba(88, 166, 255, 0)')
|
|
|
|
pointsChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: generateDateLabels(7),
|
|
datasets: [{
|
|
label: 'Points',
|
|
data: generatePlaceholderData(7),
|
|
borderColor: '#58a6ff',
|
|
backgroundColor: gradient,
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 4,
|
|
pointBackgroundColor: '#58a6ff'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
x: { grid: { color: '#21262d' }, ticks: { color: '#8b949e' } },
|
|
y: { grid: { color: '#21262d' }, ticks: { color: '#8b949e' }, beginAtZero: true }
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function initActivityChart() {
|
|
const ctx = document.getElementById('activityChart')
|
|
if (!ctx) return
|
|
|
|
activityChart = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Searches', 'Daily Set', 'Punch Cards', 'Quizzes', 'Other'],
|
|
datasets: [{
|
|
data: [45, 25, 15, 10, 5],
|
|
backgroundColor: ['#58a6ff', '#3fb950', '#d29922', '#a371f7', '#39c5cf'],
|
|
borderColor: '#161b22',
|
|
borderWidth: 3
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
cutout: '65%',
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: { color: '#8b949e', font: { size: 11 } }
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function generateDateLabels(days) {
|
|
const labels = []
|
|
for (let i = days - 1; i >= 0; i--) {
|
|
const d = new Date()
|
|
d.setDate(d.getDate() - i)
|
|
labels.push(d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }))
|
|
}
|
|
return labels
|
|
}
|
|
|
|
function generatePlaceholderData(count) {
|
|
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) {
|
|
document.querySelectorAll('.period-btn').forEach((b) => { b.classList.remove('active') })
|
|
btn.classList.add('active')
|
|
|
|
const days = period === '7d' ? 7 : 30
|
|
if (pointsChart) {
|
|
pointsChart.data.labels = generateDateLabels(days)
|
|
pointsChart.data.datasets[0].data = generatePlaceholderData(days)
|
|
pointsChart.update('none')
|
|
}
|
|
}
|
|
|
|
// Data Loading
|
|
function loadInitialData() {
|
|
fetch('/api/status').then((r) => { return r.json() }).then(updateBotStatus).catch(() => { })
|
|
fetch('/api/accounts').then((r) => { return r.json() }).then(renderAccounts).catch(() => { })
|
|
}
|
|
|
|
function refreshData() {
|
|
var btn = document.querySelector('[onclick="refreshData()"] i')
|
|
if (btn) btn.classList.add('fa-spin')
|
|
loadInitialData()
|
|
setTimeout(() => {
|
|
if (btn) btn.classList.remove('fa-spin')
|
|
showToast('Data refreshed', 'success')
|
|
}, 500)
|
|
}
|
|
|
|
// Bot Control
|
|
function startBot() {
|
|
updateButtonStates(true)
|
|
fetch('/api/start', { method: 'POST' })
|
|
.then((r) => { return r.json() })
|
|
.then((result) => {
|
|
if (result.success || result.pid) {
|
|
state.isRunning = true
|
|
state.stats.startTime = Date.now()
|
|
updateBotStatus({ running: true })
|
|
showToast('Bot started', 'success')
|
|
} else {
|
|
updateButtonStates(false)
|
|
showToast(result.error || 'Failed to start', 'error')
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
updateButtonStates(false)
|
|
showToast('Failed: ' + e.message, 'error')
|
|
})
|
|
}
|
|
|
|
function stopBot() {
|
|
fetch('/api/stop', { method: 'POST' })
|
|
.then((r) => { return r.json() })
|
|
.then((result) => {
|
|
if (result.success) {
|
|
state.isRunning = false
|
|
updateBotStatus({ running: false })
|
|
showToast('Bot stopped', 'info')
|
|
} else {
|
|
showToast(result.error || 'Failed to stop', 'error')
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
showToast('Failed: ' + e.message, 'error')
|
|
})
|
|
}
|
|
|
|
function restartBot() {
|
|
showToast('Restarting...', 'info')
|
|
fetch('/api/restart', { method: 'POST' })
|
|
.then((r) => { return r.json() })
|
|
.then((result) => {
|
|
if (result.success) {
|
|
state.stats.startTime = Date.now()
|
|
showToast('Bot restarted', 'success')
|
|
} else {
|
|
showToast(result.error || 'Failed to restart', 'error')
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
showToast('Failed: ' + e.message, 'error')
|
|
})
|
|
}
|
|
|
|
function resetJobState() {
|
|
showModal('Reset Job State',
|
|
'<p>This will clear all completed task records for today.</p>' +
|
|
'<p style="color: var(--accent-orange); margin-top: 1rem;">' +
|
|
'<i class="fas fa-exclamation-triangle"></i> This cannot be undone.</p>',
|
|
[
|
|
{ text: 'Cancel', cls: 'btn btn-secondary', action: 'closeModal()' },
|
|
{ text: 'Reset', cls: 'btn btn-danger', action: 'confirmResetJobState()' }
|
|
]
|
|
)
|
|
}
|
|
|
|
function confirmResetJobState() {
|
|
closeModal()
|
|
fetch('/api/reset-state', { method: 'POST' })
|
|
.then((r) => { return r.json() })
|
|
.then((result) => {
|
|
if (result.success) {
|
|
showToast('Job state reset', 'success')
|
|
state.stats.completed = 0
|
|
state.stats.errors = 0
|
|
updateStatsDisplay()
|
|
} else {
|
|
showToast(result.error || 'Failed to reset', 'error')
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
showToast('Failed: ' + e.message, 'error')
|
|
})
|
|
}
|
|
|
|
function updateButtonStates(running) {
|
|
var btnStart = document.getElementById('btnStart')
|
|
var btnStop = document.getElementById('btnStop')
|
|
if (btnStart) btnStart.disabled = running
|
|
if (btnStop) btnStop.disabled = !running
|
|
}
|
|
|
|
// Status Updates
|
|
function updateBotStatus(status) {
|
|
state.isRunning = status.running
|
|
updateButtonStates(status.running)
|
|
|
|
var badge = document.getElementById('statusBadge')
|
|
if (badge) {
|
|
badge.className = 'status-badge ' + (status.running ? 'status-running' : 'status-stopped')
|
|
badge.innerHTML = '<i class="fas fa-circle"></i><span>' + (status.running ? 'RUNNING' : 'STOPPED') + '</span>'
|
|
}
|
|
|
|
if (status.startTime) {
|
|
state.stats.startTime = new Date(status.startTime).getTime()
|
|
}
|
|
}
|
|
|
|
function updateStats(stats) {
|
|
if (stats.totalAccounts !== undefined) state.stats.totalAccounts = stats.totalAccounts
|
|
if (stats.totalPoints !== undefined) state.stats.totalPoints = stats.totalPoints
|
|
if (stats.completed !== undefined) state.stats.completed = stats.completed
|
|
if (stats.errors !== undefined) state.stats.errors = stats.errors
|
|
updateStatsDisplay()
|
|
}
|
|
|
|
function updateStatsDisplay() {
|
|
var el
|
|
el = document.getElementById('totalAccounts')
|
|
if (el) el.textContent = state.stats.totalAccounts
|
|
el = document.getElementById('totalPoints')
|
|
if (el) el.textContent = state.stats.totalPoints.toLocaleString()
|
|
el = document.getElementById('completed')
|
|
if (el) el.textContent = state.stats.completed
|
|
el = document.getElementById('errors')
|
|
if (el) el.textContent = state.stats.errors
|
|
el = document.getElementById('accountsBadge')
|
|
if (el) el.textContent = state.stats.totalAccounts
|
|
}
|
|
|
|
// Accounts
|
|
function renderAccounts(accounts) {
|
|
state.accounts = accounts
|
|
state.stats.totalAccounts = accounts.length
|
|
|
|
var container = document.getElementById('accountsList')
|
|
if (!container) return
|
|
|
|
if (accounts.length === 0) {
|
|
container.innerHTML = '<div class="log-empty">No accounts configured</div>'
|
|
return
|
|
}
|
|
|
|
var html = ''
|
|
accounts.forEach((account) => {
|
|
var initial = (account.email || 'U')[0].toUpperCase()
|
|
var displayEmail = account.email ? maskEmail(account.email) : 'Unknown'
|
|
var statusClass = account.status || 'pending'
|
|
var statusText = statusClass.charAt(0).toUpperCase() + statusClass.slice(1)
|
|
|
|
html += '<div class="account-item" data-email="' + (account.email || '') + '">' +
|
|
'<div class="account-info">' +
|
|
'<div class="account-avatar">' + initial + '</div>' +
|
|
'<span class="account-email">' + displayEmail + '</span>' +
|
|
'</div>' +
|
|
'<span class="account-status ' + statusClass + '">' + statusText + '</span>' +
|
|
'</div>'
|
|
})
|
|
container.innerHTML = html
|
|
updateStatsDisplay()
|
|
}
|
|
|
|
function updateAccountStatus(data) {
|
|
var accountEl = document.querySelector('.account-item[data-email="' + data.email + '"]')
|
|
if (accountEl) {
|
|
var statusEl = accountEl.querySelector('.account-status')
|
|
if (statusEl) {
|
|
statusEl.className = 'account-status ' + data.status
|
|
statusEl.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
function maskEmail(email) {
|
|
if (!email) return 'Unknown'
|
|
var parts = email.split('@')
|
|
if (parts.length < 2) return email
|
|
var local = parts[0]
|
|
var domain = parts[1]
|
|
var masked = local.length > 3 ? local.slice(0, 2) + '***' + local.slice(-1) : local[0] + '***'
|
|
return masked + '@' + domain
|
|
}
|
|
|
|
// Logging
|
|
function addLogEntry(log) {
|
|
var container = document.getElementById('logsContainer')
|
|
if (!container) return
|
|
|
|
var emptyMsg = container.querySelector('.log-empty')
|
|
if (emptyMsg) emptyMsg.remove()
|
|
|
|
var normalizedLog = {
|
|
timestamp: log.timestamp || new Date().toISOString(),
|
|
level: log.level || 'log',
|
|
source: log.source || log.title || 'BOT',
|
|
message: log.message || ''
|
|
}
|
|
|
|
state.logs.push(normalizedLog)
|
|
|
|
if (state.currentLogFilter !== 'all' && normalizedLog.level !== state.currentLogFilter) {
|
|
return
|
|
}
|
|
|
|
var entry = document.createElement('div')
|
|
entry.className = 'log-entry'
|
|
entry.innerHTML = '<span class="log-time">' + formatTime(normalizedLog.timestamp) + '</span>' +
|
|
'<span class="log-level ' + normalizedLog.level + '">' + normalizedLog.level + '</span>' +
|
|
'<span class="log-source">[' + normalizedLog.source + ']</span>' +
|
|
'<span class="log-message">' + escapeHtml(normalizedLog.message) + '</span>'
|
|
|
|
container.appendChild(entry)
|
|
|
|
while (container.children.length > 500) {
|
|
container.removeChild(container.firstChild)
|
|
}
|
|
|
|
if (state.autoScroll) {
|
|
container.scrollTop = container.scrollHeight
|
|
}
|
|
}
|
|
|
|
function filterLogs() {
|
|
var filter = document.getElementById('logFilter')
|
|
if (!filter) return
|
|
|
|
state.currentLogFilter = filter.value
|
|
|
|
var container = document.getElementById('logsContainer')
|
|
if (!container) return
|
|
|
|
container.innerHTML = ''
|
|
|
|
var filtered = state.currentLogFilter === 'all'
|
|
? state.logs
|
|
: state.logs.filter((log) => { return log.level === state.currentLogFilter })
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = '<div class="log-empty">No logs to display</div>'
|
|
return
|
|
}
|
|
|
|
filtered.forEach((log) => {
|
|
var entry = document.createElement('div')
|
|
entry.className = 'log-entry'
|
|
entry.innerHTML = '<span class="log-time">' + formatTime(log.timestamp) + '</span>' +
|
|
'<span class="log-level ' + log.level + '">' + log.level + '</span>' +
|
|
'<span class="log-source">[' + log.source + ']</span>' +
|
|
'<span class="log-message">' + escapeHtml(log.message) + '</span>'
|
|
container.appendChild(entry)
|
|
})
|
|
}
|
|
|
|
function clearLogs() {
|
|
state.logs = []
|
|
var container = document.getElementById('logsContainer')
|
|
if (container) {
|
|
container.innerHTML = '<div class="log-empty">No logs to display</div>'
|
|
}
|
|
showToast('Logs cleared', 'info')
|
|
}
|
|
|
|
function toggleAutoScroll() {
|
|
state.autoScroll = !state.autoScroll
|
|
var btn = document.getElementById('btnAutoScroll')
|
|
if (btn) {
|
|
btn.classList.toggle('btn-primary', state.autoScroll)
|
|
btn.classList.toggle('btn-secondary', !state.autoScroll)
|
|
}
|
|
showToast('Auto-scroll ' + (state.autoScroll ? 'enabled' : 'disabled'), 'info')
|
|
}
|
|
|
|
// Quick Actions
|
|
function runSingleAccount() {
|
|
if (state.accounts.length === 0) {
|
|
showToast('No accounts available', 'warning')
|
|
return
|
|
}
|
|
|
|
var options = state.accounts.map((a) => {
|
|
return '<option value="' + a.email + '">' + maskEmail(a.email) + '</option>'
|
|
}).join('')
|
|
|
|
showModal('Run Single Account',
|
|
'<p style="margin-bottom: 1rem;">Select an account to run:</p>' +
|
|
'<select id="singleAccountSelect" class="log-filter" style="width: 100%; padding: 0.5rem;">' +
|
|
options + '</select>',
|
|
[
|
|
{ text: 'Cancel', cls: 'btn btn-secondary', action: 'closeModal()' },
|
|
{ text: 'Run', cls: 'btn btn-primary', action: 'executeSingleAccount()' }
|
|
]
|
|
)
|
|
}
|
|
|
|
function executeSingleAccount() {
|
|
var select = document.getElementById('singleAccountSelect')
|
|
if (!select) return
|
|
closeModal()
|
|
showToast('Running account: ' + maskEmail(select.value), 'info')
|
|
}
|
|
|
|
function exportLogs() {
|
|
if (state.logs.length === 0) {
|
|
showToast('No logs to export', 'warning')
|
|
return
|
|
}
|
|
|
|
var logText = state.logs.map((log) => {
|
|
return '[' + formatTime(log.timestamp) + '] [' + (log.level || 'LOG').toUpperCase() + '] [' + (log.source || 'BOT') + '] ' + log.message
|
|
}).join('\n')
|
|
|
|
var blob = new Blob([logText], { type: 'text/plain' })
|
|
var url = URL.createObjectURL(blob)
|
|
var a = document.createElement('a')
|
|
a.href = url
|
|
a.download = 'rewards-bot-logs-' + new Date().toISOString().slice(0, 10) + '.txt'
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
showToast('Logs exported', 'success')
|
|
}
|
|
|
|
function openConfig() {
|
|
showToast('Config editor coming soon', 'info')
|
|
}
|
|
|
|
function viewHistory() {
|
|
showToast('History viewer coming soon', 'info')
|
|
}
|
|
|
|
// UI Utilities
|
|
function showToast(message, type) {
|
|
type = type || 'info'
|
|
var container = document.getElementById('toastContainer')
|
|
if (!container) return
|
|
|
|
var toast = document.createElement('div')
|
|
toast.className = 'toast ' + type
|
|
|
|
var icons = {
|
|
success: 'fa-check-circle',
|
|
error: 'fa-times-circle',
|
|
warning: 'fa-exclamation-circle',
|
|
info: 'fa-info-circle'
|
|
}
|
|
|
|
toast.innerHTML = '<i class="fas ' + (icons[type] || icons.info) + '"></i><span>' + message + '</span>'
|
|
container.appendChild(toast)
|
|
|
|
setTimeout(() => {
|
|
toast.style.animation = 'slideIn 0.3s ease reverse'
|
|
setTimeout(() => { toast.remove() }, 300)
|
|
}, 4000)
|
|
}
|
|
|
|
function showModal(title, body, buttons) {
|
|
var modal = document.getElementById('modal')
|
|
var modalTitle = document.getElementById('modalTitle')
|
|
var modalBody = document.getElementById('modalBody')
|
|
var modalFooter = document.getElementById('modalFooter')
|
|
|
|
if (!modal || !modalTitle || !modalBody || !modalFooter) return
|
|
|
|
modalTitle.textContent = title
|
|
modalBody.innerHTML = body
|
|
|
|
var footerHtml = '';
|
|
(buttons || []).forEach((btn) => {
|
|
footerHtml += '<button class="' + btn.cls + '" onclick="' + btn.action + '">' + btn.text + '</button>'
|
|
})
|
|
modalFooter.innerHTML = footerHtml
|
|
|
|
modal.classList.add('show')
|
|
}
|
|
|
|
function closeModal() {
|
|
var modal = document.getElementById('modal')
|
|
if (modal) modal.classList.remove('show')
|
|
}
|
|
|
|
// Theme
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('light-theme')
|
|
var isLight = document.body.classList.contains('light-theme')
|
|
try {
|
|
localStorage.setItem('theme', isLight ? 'light' : 'dark')
|
|
} catch (e) { }
|
|
|
|
var btn = document.querySelector('.theme-toggle i')
|
|
if (btn) btn.className = isLight ? 'fas fa-sun' : 'fas fa-moon'
|
|
|
|
updateChartsTheme(isLight)
|
|
}
|
|
|
|
function loadTheme() {
|
|
try {
|
|
var theme = localStorage.getItem('theme')
|
|
if (theme === 'light') {
|
|
document.body.classList.add('light-theme')
|
|
var btn = document.querySelector('.theme-toggle i')
|
|
if (btn) btn.className = 'fas fa-sun'
|
|
updateChartsTheme(true)
|
|
}
|
|
} catch (e) { }
|
|
}
|
|
|
|
function updateChartsTheme(isLight) {
|
|
var gridColor = isLight ? '#eaeef2' : '#21262d'
|
|
var textColor = isLight ? '#656d76' : '#8b949e'
|
|
|
|
if (pointsChart) {
|
|
pointsChart.options.scales.x.grid.color = gridColor
|
|
pointsChart.options.scales.y.grid.color = gridColor
|
|
pointsChart.options.scales.x.ticks.color = textColor
|
|
pointsChart.options.scales.y.ticks.color = textColor
|
|
pointsChart.update('none')
|
|
}
|
|
|
|
if (activityChart) {
|
|
activityChart.options.plugins.legend.labels.color = textColor
|
|
activityChart.update('none')
|
|
}
|
|
}
|
|
|
|
// Uptime Timer
|
|
function startUptimeTimer() {
|
|
setInterval(() => {
|
|
if (state.isRunning && state.stats.startTime) {
|
|
var elapsed = Date.now() - state.stats.startTime
|
|
var el = document.getElementById('uptime')
|
|
if (el) el.textContent = formatDuration(elapsed)
|
|
}
|
|
}, 1000)
|
|
}
|
|
|
|
// Formatting
|
|
function formatTime(timestamp) {
|
|
var d = new Date(timestamp)
|
|
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
}
|
|
|
|
function formatDuration(ms) {
|
|
var secs = Math.floor(ms / 1000)
|
|
var hrs = Math.floor(secs / 3600)
|
|
var mins = Math.floor((secs % 3600) / 60)
|
|
var s = secs % 60
|
|
return pad(hrs) + ':' + pad(mins) + ':' + pad(s)
|
|
}
|
|
|
|
function pad(n) {
|
|
return n < 10 ? '0' + n : '' + n
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
var div = document.createElement('div')
|
|
div.textContent = text
|
|
return div.innerHTML
|
|
}
|
|
|
|
// Event listeners
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('modal')) closeModal()
|
|
})
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') closeModal()
|
|
})
|