Files
Microsoft-Rewards-Bot/src/dashboard/routes.ts

306 lines
9.2 KiB
TypeScript

import { Router, Request, Response } from 'express'
import fs from 'fs'
import path from 'path'
import { dashboardState } from './state'
import { loadAccounts, loadConfig, getConfigPath } from '../util/Load'
import { spawn } from 'child_process'
export const apiRouter = Router()
// GET /api/status - Bot status
apiRouter.get('/status', (_req: Request, res: Response) => {
try {
const accounts = dashboardState.getAccounts()
// If no accounts loaded yet, try to load them
if (accounts.length === 0) {
try {
const loadedAccounts = loadAccounts()
dashboardState.initializeAccounts(loadedAccounts.map(a => a.email))
} catch (error) {
console.error('[Dashboard] Failed to load accounts for status:', error)
}
}
res.json(dashboardState.getStatus())
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// GET /api/accounts - List all accounts with masked emails
apiRouter.get('/accounts', (_req: Request, res: Response) => {
try {
let accounts = dashboardState.getAccounts()
// If no accounts in state, try to load from config
if (accounts.length === 0) {
try {
const loadedAccounts = loadAccounts()
dashboardState.initializeAccounts(loadedAccounts.map(a => a.email))
accounts = dashboardState.getAccounts()
} catch (error) {
console.error('[Dashboard] Failed to load accounts:', error)
}
}
res.json(accounts)
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// GET /api/logs - Recent logs
apiRouter.get('/logs', (req: Request, res: Response) => {
try {
const limit = parseInt(req.query.limit as string) || 100
const logs = dashboardState.getLogs(Math.min(limit, 500))
res.json(logs)
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// DELETE /api/logs - Clear logs
apiRouter.delete('/logs', (_req: Request, res: Response) => {
try {
dashboardState.clearLogs()
res.json({ success: true })
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// GET /api/history - Recent run summaries
apiRouter.get('/history', (_req: Request, res: Response): void => {
try {
const reportsDir = path.join(process.cwd(), 'reports')
if (!fs.existsSync(reportsDir)) {
res.json([])
return
}
const days = fs.readdirSync(reportsDir).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().slice(0, 7)
const summaries: unknown[] = []
for (const day of days) {
const dayDir = path.join(reportsDir, day)
const files = fs.readdirSync(dayDir).filter(f => f.startsWith('summary_') && f.endsWith('.json'))
for (const file of files) {
try {
const content = fs.readFileSync(path.join(dayDir, file), 'utf-8')
summaries.push(JSON.parse(content))
} catch {
continue
}
}
}
res.json(summaries.slice(0, 50))
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// GET /api/config - Current config (tokens masked)
apiRouter.get('/config', (_req: Request, res: Response) => {
try {
const config = loadConfig()
const safe = JSON.parse(JSON.stringify(config))
// Mask sensitive data
if (safe.webhook?.url) safe.webhook.url = maskUrl(safe.webhook.url)
if (safe.conclusionWebhook?.url) safe.conclusionWebhook.url = maskUrl(safe.conclusionWebhook.url)
if (safe.ntfy?.authToken) safe.ntfy.authToken = '***'
res.json(safe)
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// POST /api/config - Update config (with backup)
apiRouter.post('/config', (req: Request, res: Response): void => {
try {
const newConfig = req.body
const configPath = getConfigPath()
if (!configPath || !fs.existsSync(configPath)) {
res.status(404).json({ error: 'Config file not found' })
return
}
// Backup current config
const backupPath = `${configPath}.backup.${Date.now()}`
fs.copyFileSync(configPath, backupPath)
// Write new config
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf-8')
res.json({ success: true, backup: backupPath })
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// POST /api/start - Start bot in background
apiRouter.post('/start', (_req: Request, res: Response): void => {
try {
const status = dashboardState.getStatus()
if (status.running) {
res.status(400).json({ error: 'Bot already running' })
return
}
// Set running state
dashboardState.setRunning(true)
// Log the start
dashboardState.addLog({
timestamp: new Date().toISOString(),
level: 'log',
platform: 'MAIN',
title: 'DASHBOARD',
message: 'Bot start requested from dashboard'
})
res.json({ success: true, message: 'Bot start requested. Check logs for progress.' })
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// POST /api/stop - Stop bot
apiRouter.post('/stop', (_req: Request, res: Response) => {
try {
dashboardState.setRunning(false)
dashboardState.addLog({
timestamp: new Date().toISOString(),
level: 'warn',
platform: 'MAIN',
title: 'DASHBOARD',
message: 'Bot stop requested from dashboard'
})
res.json({ success: true, message: 'Bot will stop after current task completes' })
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// POST /api/sync/:email - Force sync single account
apiRouter.post('/sync/:email', async (req: Request, res: Response): Promise<void> => {
try {
const { email } = req.params
if (!email) {
res.status(400).json({ error: 'Email parameter required' })
return
}
const accounts = loadAccounts()
const account = accounts.find(a => a.email === email)
if (!account) {
res.status(404).json({ error: 'Account not found' })
return
}
dashboardState.updateAccount(email, { status: 'running', lastSync: new Date().toISOString() })
// Spawn single account run
const child = spawn(process.execPath, [
path.join(process.cwd(), 'dist', 'index.js'),
'-account',
email
], { detached: true, stdio: 'ignore' })
if (child.unref) child.unref()
res.json({ success: true, pid: child.pid || undefined })
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// GET /api/metrics - Basic metrics
apiRouter.get('/metrics', (_req: Request, res: Response) => {
try {
const accounts = dashboardState.getAccounts()
const totalPoints = accounts.reduce((sum, a) => sum + (a.points || 0), 0)
const accountsWithErrors = accounts.filter(a => a.errors && a.errors.length > 0).length
const avgPoints = accounts.length > 0 ? Math.round(totalPoints / accounts.length) : 0
res.json({
totalAccounts: accounts.length,
totalPoints,
avgPoints,
accountsWithErrors,
accountsRunning: accounts.filter(a => a.status === 'running').length,
accountsCompleted: accounts.filter(a => a.status === 'completed').length,
accountsIdle: accounts.filter(a => a.status === 'idle').length,
accountsError: accounts.filter(a => a.status === 'error').length
})
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// GET /api/account/:email - Get specific account details
apiRouter.get('/account/:email', (req: Request, res: Response): void => {
try {
const { email } = req.params
if (!email) {
res.status(400).json({ error: 'Email parameter required' })
return
}
const account = dashboardState.getAccount(email)
if (!account) {
res.status(404).json({ error: 'Account not found' })
return
}
res.json(account)
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
// POST /api/account/:email/reset - Reset account status
apiRouter.post('/account/:email/reset', (req: Request, res: Response): void => {
try {
const { email } = req.params
if (!email) {
res.status(400).json({ error: 'Email parameter required' })
return
}
const account = dashboardState.getAccount(email)
if (!account) {
res.status(404).json({ error: 'Account not found' })
return
}
dashboardState.updateAccount(email, {
status: 'idle',
errors: []
})
res.json({ success: true })
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
}
})
function maskUrl(url: string): string {
try {
const parsed = new URL(url)
return `${parsed.protocol}//${parsed.hostname.slice(0, 3)}***${parsed.pathname.slice(0, 5)}***`
} catch {
return '***'
}
}