diff --git a/public/app.js b/public/app.js index 2e0192c..87e1b68 100644 --- a/public/app.js +++ b/public/app.js @@ -220,6 +220,88 @@ function setChartPeriod(period, btn) { function loadInitialData() { fetch('/api/status').then((r) => { return r.json() }).then(updateBotStatus).catch(() => { }) fetch('/api/accounts').then((r) => { return r.json() }).then(renderAccounts).catch(() => { }) + loadAccountHistoryData() // FIXED: Load real historical data for charts +} + +// FIXED: Load account history from API to populate charts with real data +function loadAccountHistoryData() { + fetch('/api/account-history') + .then((r) => r.json()) + .then((histories) => { + if (histories && Object.keys(histories).length > 0) { + updateChartsWithRealData(histories) + } + }) + .catch((e) => { + console.warn('[History] Failed to load:', e) + }) +} + +// FIXED: Update charts with real historical data +function updateChartsWithRealData(histories) { + var last7Days = {} + var activityCounts = { + 'Desktop Search': 0, + 'Mobile Search': 0, + 'Daily Set': 0, + 'Quizzes': 0, + 'Other': 0 + } + + // Process each account's history + Object.values(histories).forEach((accountHistory) => { + if (!accountHistory.history) return + + accountHistory.history.forEach((entry) => { + var date = entry.date || entry.timestamp.split('T')[0] + + // Aggregate points by day + if (!last7Days[date]) { + last7Days[date] = 0 + } + last7Days[date] += entry.totalPoints || 0 + + // Count activity types + if (entry.completedActivities) { + entry.completedActivities.forEach((activity) => { + if (activity.includes('Search')) { + if (entry.desktopPoints > 0) activityCounts['Desktop Search']++ + if (entry.mobilePoints > 0) activityCounts['Mobile Search']++ + } else if (activity.includes('DailySet')) { + activityCounts['Daily Set']++ + } else if (activity.includes('Quiz') || activity.includes('Poll')) { + activityCounts['Quizzes']++ + } else { + activityCounts['Other']++ + } + }) + } + }) + }) + + // Update points chart with last 7 days + if (pointsChart) { + var sortedDates = Object.keys(last7Days).sort().slice(-7) + var labels = sortedDates.map((d) => { + var date = new Date(d) + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + }) + var data = sortedDates.map((d) => { return last7Days[d] }) + + pointsChart.data.labels = labels.length > 0 ? labels : generateDateLabels(7) + pointsChart.data.datasets[0].data = data.length > 0 ? data : generatePlaceholderData(7) + pointsChart.update() + } + + // Update activity chart + if (activityChart) { + var total = Object.values(activityCounts).reduce((sum, val) => { return sum + val }, 0) + if (total > 0) { + activityChart.data.labels = Object.keys(activityCounts) + activityChart.data.datasets[0].data = Object.values(activityCounts) + activityChart.update() + } + } } function refreshData() { @@ -531,8 +613,36 @@ function runSingleAccount() { function executeSingleAccount() { var select = document.getElementById('singleAccountSelect') if (!select) return + + var email = select.value closeModal() - showToast('Running account: ' + maskEmail(select.value), 'info') + + if (!email) { + showToast('No account selected', 'error') + return + } + + showToast('Starting bot for: ' + maskEmail(email), 'info') + + // Call API to run single account + fetch('/api/run-single', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email }) + }) + .then((res) => res.json()) + .then((data) => { + if (data.success) { + showToast('✓ Bot started for account: ' + maskEmail(email), 'success') + loadStatus() + } else { + showToast('✗ Failed to start: ' + (data.error || 'Unknown error'), 'error') + } + }) + .catch((err) => { + console.error('[API] Run single failed:', err) + showToast('✗ Request failed: ' + err.message, 'error') + }) } function exportLogs() { diff --git a/src/config.jsonc b/src/config.jsonc index a626620..948c125 100644 --- a/src/config.jsonc +++ b/src/config.jsonc @@ -88,7 +88,7 @@ }, // === BROWSER === "browser": { - "headless": true, + "headless": false, "globalTimeout": "30s" }, "fingerprinting": { diff --git a/src/dashboard/BotController.ts b/src/dashboard/BotController.ts index b131de9..d1101ba 100644 --- a/src/dashboard/BotController.ts +++ b/src/dashboard/BotController.ts @@ -106,6 +106,81 @@ export class BotController { return await this.start() } + /** + * Run a single account (for dashboard "run single" feature) + * FIXED: Actually implement single account execution + */ + public async runSingle(email: string): Promise<{ success: boolean; error?: string; pid?: number }> { + if (this.botInstance) { + return { success: false, error: 'Bot is already running. Stop it first.' } + } + + if (this.isStarting) { + return { success: false, error: 'Bot is currently starting, please wait' } + } + + try { + this.isStarting = true + this.log(`🚀 Starting bot for single account: ${email}`, 'log') + + const { MicrosoftRewardsBot } = await import('../index') + const { loadAccounts } = await import('../util/state/Load') + + // Load all accounts and filter to just this one + const allAccounts = loadAccounts() + const targetAccount = allAccounts.find(a => a.email === email) + + if (!targetAccount) { + return { success: false, error: `Account ${email} not found in accounts.jsonc` } + } + + this.botInstance = new MicrosoftRewardsBot(false) + this.startTime = new Date() + dashboardState.setRunning(true) + dashboardState.setBotInstance(this.botInstance) + + // Update account status + dashboardState.updateAccount(email, { status: 'running', errors: [] }) + + // Run bot asynchronously with single account + void (async () => { + try { + this.log(`✓ Bot initialized for ${email}, starting execution...`, 'log') + + await this.botInstance!.initialize() + + // Override accounts to run only this one + ; (this.botInstance as any).accounts = [targetAccount] + + await this.botInstance!.run() + + this.log(`✓ Bot completed successfully for ${email}`, 'log') + dashboardState.updateAccount(email, { status: 'completed' }) + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error) + this.log(`Bot error for ${email}: ${errMsg}`, 'error') + dashboardState.updateAccount(email, { + status: 'error', + errors: [errMsg] + }) + } finally { + this.cleanup() + } + })() + + return { success: true, pid: process.pid } + + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + this.log(`Failed to start bot for ${email}: ${errorMsg}`, 'error') + dashboardState.updateAccount(email, { status: 'error', errors: [errorMsg] }) + this.cleanup() + return { success: false, error: errorMsg } + } finally { + this.isStarting = false + } + } + private async wait(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } diff --git a/src/dashboard/routes.ts b/src/dashboard/routes.ts index 3606e18..394bd48 100644 --- a/src/dashboard/routes.ts +++ b/src/dashboard/routes.ts @@ -199,6 +199,34 @@ apiRouter.post('/restart', async (_req: Request, res: Response): Promise = } }) +// POST /api/run-single - Run a single account (dashboard feature) +apiRouter.post('/run-single', async (req: Request, res: Response): Promise => { + try { + const { email } = req.body + + if (!email) { + sendError(res, 400, 'Email is required') + return + } + + const status = botController.getStatus() + if (status.running) { + sendError(res, 400, `Bot already running (PID: ${status.pid}). Stop it first.`) + return + } + + const result = await botController.runSingle(email) + + if (result.success) { + sendSuccess(res, { message: `Started bot for account ${email}`, pid: result.pid }) + } else { + sendError(res, 500, result.error || 'Failed to start bot for account') + } + } catch (error) { + sendError(res, 500, getErr(error)) + } +}) + // GET /api/metrics - Basic metrics apiRouter.get('/metrics', (_req: Request, res: Response) => { try {