From ae4e34cd6608f667ff230a2ad9c00f24b1788c38 Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Mon, 3 Nov 2025 23:07:10 +0100 Subject: [PATCH] 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 --- README.md | 2 +- docs/dashboard-quickstart.md | 213 ++++++++ public/dashboard.html | 946 ++++++++++++++++++++++++++++++++++ public/favicon.ico | 1 + public/index.html | 964 +++++++++++++++++++++++++++++++++++ src/dashboard/routes.ts | 91 +++- src/dashboard/server.ts | 76 ++- src/dashboard/state.ts | 59 ++- src/index.ts | 16 + 9 files changed, 2343 insertions(+), 25 deletions(-) create mode 100644 docs/dashboard-quickstart.md create mode 100644 public/dashboard.html create mode 100644 public/favicon.ico create mode 100644 public/index.html diff --git a/README.md b/README.md index e56146b..c2d3822 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ For detailed configuration, advanced features, and troubleshooting, visit our co --- -## 📊 Dashboard (NEW) +## 📊 Dashboard (BETA) Monitor and control your bot through a local web interface: diff --git a/docs/dashboard-quickstart.md b/docs/dashboard-quickstart.md new file mode 100644 index 0000000..1263eb4 --- /dev/null +++ b/docs/dashboard-quickstart.md @@ -0,0 +1,213 @@ +# 🚀 Dashboard Quick Start Guide + +## What's New? + +The dashboard has been completely redesigned with: +- ✨ Modern, beautiful interface with gradients and animations +- 🔄 Real-time updates via WebSocket +- 📊 Enhanced statistics and metrics +- 🎮 Better control panel +- 📱 Fully responsive design +- 🎨 Professional UI/UX + +## Getting Started + +### Option 1: Standalone Dashboard (Recommended for Testing) + +Start only the dashboard to see the interface: + +```bash +npm run build +npm run dashboard +``` + +Then open: **http://localhost:3000** + +### Option 2: Enable with Bot + +1. Edit `src/config.jsonc`: + +```jsonc +{ + "dashboard": { + "enabled": true, + "port": 3000, + "host": "127.0.0.1" + } +} +``` + +2. Start the bot: + +```bash +npm start +``` + +Dashboard will be available at: **http://localhost:3000** + +## What You'll See + +### 📊 Header +- Bot logo and title +- Real-time status badge (RUNNING/STOPPED with animated indicator) +- Last run timestamp + +### 📈 Statistics Cards +- **Total Accounts** - Number of configured accounts +- **Total Points** - Sum of all points across accounts +- **Completed** - Successfully processed accounts +- **Errors** - Accounts with issues + +### 🎮 Control Panel +- **Start Bot** - Begin automation (shows when stopped) +- **Stop Bot** - Halt execution (shows when running) +- **Refresh** - Update all data manually +- **Clear Logs** - Remove log history + +### 👥 Accounts Section +- List of all accounts with masked emails +- Current points for each account +- Status badge (idle, running, completed, error) +- Last sync timestamp + +### 📋 Live Logs +- Real-time streaming logs +- Color-coded by level: + - 🟢 Green = Info + - 🟡 Yellow = Warning + - 🔴 Red = Error +- Platform indicators (MAIN, DESKTOP, MOBILE) +- Timestamps for each entry +- Auto-scrolling to latest + +## Features to Try + +### 1. Real-Time Updates +- Open dashboard while bot is running +- Watch logs appear instantly +- See account status change in real-time +- Notice points increment live + +### 2. Control Bot +- Click "Start Bot" to begin automation +- Watch status badge change to RUNNING +- See logs stream in +- Click "Stop Bot" to halt + +### 3. View Account Details +- Each account shows current points +- Status badge shows current state +- Last sync shows when it was processed + +### 4. Manage Logs +- Logs auto-update as they happen +- Scroll through history +- Click "Clear Logs" to start fresh +- Logs persist in memory (up to 500 entries) + +## API Access + +You can also use the API directly: + +### Get Status +```bash +curl http://localhost:3000/api/status +``` + +### Get All Accounts +```bash +curl http://localhost:3000/api/accounts +``` + +### Get Logs +```bash +curl http://localhost:3000/api/logs?limit=50 +``` + +### Get Metrics +```bash +curl http://localhost:3000/api/metrics +``` + +### Control Bot +```bash +# Start +curl -X POST http://localhost:3000/api/start + +# Stop +curl -X POST http://localhost:3000/api/stop +``` + +## WebSocket Testing + +Test real-time WebSocket connection: + +```javascript +// Open browser console at http://localhost:3000 +const ws = new WebSocket('ws://localhost:3000'); + +ws.onopen = () => console.log('✓ Connected'); + +ws.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log('Received:', data); +}; +``` + +## Troubleshooting + +### "Cannot GET /" +- Run `npm run build` first +- Check `public/index.html` exists +- Try `npm run dashboard-dev` instead + +### No Accounts Showing +- Ensure `src/accounts.jsonc` is configured +- Check file exists and has valid JSON +- Refresh the page + +### WebSocket Not Connected +- Check dashboard server is running +- Look for "WebSocket connected" in browser console +- Try refreshing the page + +### Port Already in Use +Change the port: +```bash +# In config.jsonc +"dashboard": { + "port": 3001 // Use different port +} +``` + +Or use environment variable: +```bash +DASHBOARD_PORT=3001 npm run dashboard +``` + +## Next Steps + +1. **Customize Theme** - Edit colors in `public/index.html` +2. **Add Features** - Extend API in `src/dashboard/routes.ts` +3. **Monitor Bot** - Leave dashboard open while bot runs +4. **Use API** - Build custom integrations +5. **Deploy** - Set up reverse proxy for remote access + +## Tips + +- 💡 Keep dashboard open in a browser tab while bot runs +- 💡 Use Refresh button if data seems stale +- 💡 Clear logs periodically for better performance +- 💡 Check browser console for WebSocket status +- 💡 Use API for automated monitoring scripts + +## Need Help? + +- 📖 Read full documentation in `src/dashboard/DASHBOARD.md` +- 🔍 Check API reference in `src/dashboard/README.md` +- 🐛 Report issues on GitHub +- 💬 Ask in community Discord + +--- + +**Enjoy your new dashboard! 🎉** diff --git a/public/dashboard.html b/public/dashboard.html new file mode 100644 index 0000000..d4597b6 --- /dev/null +++ b/public/dashboard.html @@ -0,0 +1,946 @@ + + + + + + Microsoft Rewards Bot - Dashboard + + + + + + + + + + + + + +
+ + + + + + + + + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..448c33c --- /dev/null +++ b/public/favicon.ico @@ -0,0 +1 @@ + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f250f69 --- /dev/null +++ b/public/index.html @@ -0,0 +1,964 @@ + + + + + + Microsoft Rewards Bot - Dashboard + + + + + + + + + + + + + +
+ + + + + + + + + diff --git a/src/dashboard/routes.ts b/src/dashboard/routes.ts index e79f78d..5e76509 100644 --- a/src/dashboard/routes.ts +++ b/src/dashboard/routes.ts @@ -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) diff --git a/src/dashboard/server.ts b/src/dashboard/server.ts index 3033fad..89c6430 100644 --- a/src/dashboard/server.ts +++ b/src/dashboard/server.ts @@ -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(` + + Dashboard - API Only Mode + +

Dashboard API Active

+

Frontend UI not found. API endpoints are available:

+ + + `) + } }) } @@ -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}`) diff --git a/src/dashboard/state.ts b/src/dashboard/state.ts index cf3bedd..6d18f65 100644 --- a/src/dashboard/state.ts +++ b/src/dashboard/state.ts @@ -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 = new Map() private maxLogsInMemory = 500 + private changeListeners: Set = 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): 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() diff --git a/src/index.ts b/src/index.ts index e65bf0c..b7e6b24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1325,7 +1325,18 @@ async function main() { // Check for dashboard mode flag (standalone dashboard) if (process.argv.includes('-dashboard')) { const { startDashboardServer } = await import('./dashboard/server') + const { dashboardState } = await import('./dashboard/state') log('main', 'DASHBOARD', 'Starting standalone dashboard server...') + + // Load and initialize accounts + try { + const accounts = loadAccounts() + dashboardState.initializeAccounts(accounts.map(a => a.email)) + log('main', 'DASHBOARD', `Initialized ${accounts.length} accounts in dashboard`) + } catch (error) { + log('main', 'DASHBOARD', 'Could not load accounts: ' + (error instanceof Error ? error.message : String(error)), 'warn') + } + startDashboardServer() return } @@ -1338,6 +1349,7 @@ async function main() { // Auto-start dashboard if enabled in config if (config.dashboard?.enabled) { const { DashboardServer } = await import('./dashboard/server') + const { dashboardState } = await import('./dashboard/state') const port = config.dashboard.port || 3000 const host = config.dashboard.host || '127.0.0.1' @@ -1345,6 +1357,10 @@ async function main() { process.env.DASHBOARD_PORT = String(port) process.env.DASHBOARD_HOST = host + // Initialize dashboard with accounts + const accounts = loadAccounts() + dashboardState.initializeAccounts(accounts.map(a => a.email)) + const dashboardServer = new DashboardServer() dashboardServer.start() log('main', 'DASHBOARD', `Auto-started dashboard on http://${host}:${port}`)