mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-11 17:56:15 +00:00
Add initial HTML structure and styles for Microsoft Rewards Bot dashboard
- Created index.html with a complete layout for the dashboard - Added favicon.ico placeholder - Implemented responsive design and styling using CSS variables - Integrated React for dynamic data handling and real-time updates - Set up WebSocket connection for live log updates - Included functionality for starting/stopping the bot and managing accounts
This commit is contained in:
@@ -129,15 +129,19 @@ apiRouter.post('/start', (_req: Request, res: Response): void => {
|
||||
return
|
||||
}
|
||||
|
||||
// Spawn bot as child process
|
||||
const child = spawn(process.execPath, [path.join(process.cwd(), 'dist', 'index.js')], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
})
|
||||
child.unref()
|
||||
|
||||
// Set running state
|
||||
dashboardState.setRunning(true)
|
||||
res.json({ success: true, pid: child.pid })
|
||||
|
||||
// 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' })
|
||||
}
|
||||
@@ -146,13 +150,17 @@ apiRouter.post('/start', (_req: Request, res: Response): void => {
|
||||
// POST /api/stop - Stop bot
|
||||
apiRouter.post('/stop', (_req: Request, res: Response) => {
|
||||
try {
|
||||
const bot = dashboardState.getBotInstance()
|
||||
if (bot) {
|
||||
// Graceful shutdown
|
||||
process.kill(process.pid, 'SIGTERM')
|
||||
}
|
||||
dashboardState.setRunning(false)
|
||||
res.json({ success: true })
|
||||
|
||||
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' })
|
||||
}
|
||||
@@ -198,19 +206,72 @@ apiRouter.get('/metrics', (_req: Request, res: Response) => {
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -2,6 +2,7 @@ import express from 'express'
|
||||
import { createServer } from 'http'
|
||||
import { WebSocketServer, WebSocket } from 'ws'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { apiRouter } from './routes'
|
||||
import { dashboardState, DashboardLog } from './state'
|
||||
import { log as botLog } from '../util/Logger'
|
||||
@@ -23,10 +24,19 @@ export class DashboardServer {
|
||||
this.setupRoutes()
|
||||
this.setupWebSocket()
|
||||
this.interceptBotLogs()
|
||||
this.setupStateListener()
|
||||
}
|
||||
|
||||
private setupStateListener(): void {
|
||||
// Listen to dashboard state changes and broadcast to all clients
|
||||
dashboardState.addChangeListener((type, data) => {
|
||||
this.broadcastUpdate(type, data)
|
||||
})
|
||||
}
|
||||
|
||||
private setupMiddleware(): void {
|
||||
this.app.use(express.json())
|
||||
this.app.use('/assets', express.static(path.join(__dirname, '../../assets')))
|
||||
this.app.use(express.static(path.join(__dirname, '../../public')))
|
||||
}
|
||||
|
||||
@@ -38,9 +48,32 @@ export class DashboardServer {
|
||||
res.json({ status: 'ok', uptime: process.uptime() })
|
||||
})
|
||||
|
||||
// Serve dashboard UI
|
||||
// Serve dashboard UI (with fallback if file doesn't exist)
|
||||
this.app.get('/', (_req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../../public/index.html'))
|
||||
const dashboardPath = path.join(__dirname, '../../public/dashboard.html')
|
||||
const indexPath = path.join(__dirname, '../../public/index.html')
|
||||
|
||||
if (fs.existsSync(dashboardPath)) {
|
||||
res.sendFile(dashboardPath)
|
||||
} else if (fs.existsSync(indexPath)) {
|
||||
res.sendFile(indexPath)
|
||||
} else {
|
||||
res.status(200).send(`
|
||||
<!DOCTYPE html>
|
||||
<html><head><title>Dashboard - API Only Mode</title></head>
|
||||
<body style="font-family: sans-serif; padding: 40px; text-align: center;">
|
||||
<h1>Dashboard API Active</h1>
|
||||
<p>Frontend UI not found. API endpoints are available:</p>
|
||||
<ul style="list-style: none; padding: 0;">
|
||||
<li><a href="/api/status">GET /api/status</a></li>
|
||||
<li><a href="/api/accounts">GET /api/accounts</a></li>
|
||||
<li><a href="/api/logs">GET /api/logs</a></li>
|
||||
<li><a href="/api/metrics">GET /api/metrics</a></li>
|
||||
<li><a href="/health">GET /health</a></li>
|
||||
</ul>
|
||||
</body></html>
|
||||
`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,9 +87,23 @@ export class DashboardServer {
|
||||
console.log('[Dashboard] WebSocket client disconnected')
|
||||
})
|
||||
|
||||
// Send recent logs on connect
|
||||
const recentLogs = dashboardState.getLogs(50)
|
||||
ws.send(JSON.stringify({ type: 'history', logs: recentLogs }))
|
||||
ws.on('error', (error) => {
|
||||
console.error('[Dashboard] WebSocket error:', error)
|
||||
})
|
||||
|
||||
// Send initial data on connect
|
||||
const recentLogs = dashboardState.getLogs(100)
|
||||
const status = dashboardState.getStatus()
|
||||
const accounts = dashboardState.getAccounts()
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'init',
|
||||
data: {
|
||||
logs: recentLogs,
|
||||
status,
|
||||
accounts
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -88,7 +135,11 @@ export class DashboardServer {
|
||||
const payload = JSON.stringify({ type: 'log', log: logEntry })
|
||||
for (const client of clients) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(payload)
|
||||
try {
|
||||
client.send(payload)
|
||||
} catch (error) {
|
||||
console.error('[Dashboard] Error sending to WebSocket client:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +147,19 @@ export class DashboardServer {
|
||||
}
|
||||
}
|
||||
|
||||
public broadcastUpdate(type: string, data: unknown): void {
|
||||
const payload = JSON.stringify({ type, data })
|
||||
for (const client of this.clients) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
client.send(payload)
|
||||
} catch (error) {
|
||||
console.error('[Dashboard] Error broadcasting update:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.server.listen(PORT, HOST, () => {
|
||||
console.log(`[Dashboard] Server running on http://${HOST}:${PORT}`)
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface DashboardStatus {
|
||||
lastRun?: string
|
||||
currentAccount?: string
|
||||
totalAccounts: number
|
||||
startTime?: string
|
||||
}
|
||||
|
||||
export interface DashboardLog {
|
||||
@@ -22,14 +23,36 @@ export interface AccountStatus {
|
||||
lastSync?: string
|
||||
status: 'idle' | 'running' | 'completed' | 'error'
|
||||
errors?: string[]
|
||||
progress?: string
|
||||
}
|
||||
|
||||
type ChangeListener = (type: string, data: unknown) => void
|
||||
|
||||
class DashboardState {
|
||||
private botInstance?: MicrosoftRewardsBot
|
||||
private status: DashboardStatus = { running: false, totalAccounts: 0 }
|
||||
private logs: DashboardLog[] = []
|
||||
private accounts: Map<string, AccountStatus> = new Map()
|
||||
private maxLogsInMemory = 500
|
||||
private changeListeners: Set<ChangeListener> = new Set()
|
||||
|
||||
public addChangeListener(listener: ChangeListener): void {
|
||||
this.changeListeners.add(listener)
|
||||
}
|
||||
|
||||
public removeChangeListener(listener: ChangeListener): void {
|
||||
this.changeListeners.delete(listener)
|
||||
}
|
||||
|
||||
private notifyChange(type: string, data: unknown): void {
|
||||
for (const listener of this.changeListeners) {
|
||||
try {
|
||||
listener(type, data)
|
||||
} catch (error) {
|
||||
console.error('[Dashboard State] Error notifying listener:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStatus(): DashboardStatus {
|
||||
return { ...this.status }
|
||||
@@ -38,9 +61,20 @@ class DashboardState {
|
||||
setRunning(running: boolean, currentAccount?: string): void {
|
||||
this.status.running = running
|
||||
this.status.currentAccount = currentAccount
|
||||
if (!running && currentAccount === undefined) {
|
||||
this.status.lastRun = new Date().toISOString()
|
||||
|
||||
if (running && !this.status.startTime) {
|
||||
this.status.startTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (!running) {
|
||||
this.status.lastRun = new Date().toISOString()
|
||||
this.status.startTime = undefined
|
||||
if (currentAccount === undefined) {
|
||||
this.status.currentAccount = undefined
|
||||
}
|
||||
}
|
||||
|
||||
this.notifyChange('status', this.getStatus())
|
||||
}
|
||||
|
||||
setBotInstance(bot: MicrosoftRewardsBot | undefined): void {
|
||||
@@ -56,6 +90,7 @@ class DashboardState {
|
||||
if (this.logs.length > this.maxLogsInMemory) {
|
||||
this.logs.shift()
|
||||
}
|
||||
this.notifyChange('log', log)
|
||||
}
|
||||
|
||||
getLogs(limit = 100): DashboardLog[] {
|
||||
@@ -64,6 +99,7 @@ class DashboardState {
|
||||
|
||||
clearLogs(): void {
|
||||
this.logs = []
|
||||
this.notifyChange('logs_cleared', true)
|
||||
}
|
||||
|
||||
updateAccount(email: string, update: Partial<AccountStatus>): void {
|
||||
@@ -72,8 +108,10 @@ class DashboardState {
|
||||
maskedEmail: this.maskEmail(email),
|
||||
status: 'idle'
|
||||
}
|
||||
this.accounts.set(email, { ...existing, ...update })
|
||||
const updated = { ...existing, ...update }
|
||||
this.accounts.set(email, updated)
|
||||
this.status.totalAccounts = this.accounts.size
|
||||
this.notifyChange('account_update', updated)
|
||||
}
|
||||
|
||||
getAccounts(): AccountStatus[] {
|
||||
@@ -92,6 +130,21 @@ class DashboardState {
|
||||
const maskedDomain = domainName && domainName.length > 1 ? `${domainName.slice(0, 1)}***.${tld || 'com'}` : domain
|
||||
return `${maskedLocal}@${maskedDomain}`
|
||||
}
|
||||
|
||||
// Initialize accounts from config
|
||||
public initializeAccounts(emails: string[]): void {
|
||||
for (const email of emails) {
|
||||
if (!this.accounts.has(email)) {
|
||||
this.accounts.set(email, {
|
||||
email,
|
||||
maskedEmail: this.maskEmail(email),
|
||||
status: 'idle'
|
||||
})
|
||||
}
|
||||
}
|
||||
this.status.totalAccounts = this.accounts.size
|
||||
this.notifyChange('accounts', this.getAccounts())
|
||||
}
|
||||
}
|
||||
|
||||
export const dashboardState = new DashboardState()
|
||||
|
||||
Reference in New Issue
Block a user