mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 17:26:17 +00:00
feat: add bot restart functionality and improve error handling in dashboard
This commit is contained in:
@@ -574,6 +574,10 @@
|
||||
border-left: 4px solid var(--danger);
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left: 4px solid var(--primary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.app-container {
|
||||
@@ -623,7 +627,7 @@
|
||||
|
||||
const showToast = (message, type = 'success') => {
|
||||
setToast({ message, type });
|
||||
setTimeout(() => setToast(null), 3000);
|
||||
setTimeout(() => setToast(null), 5000);
|
||||
};
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
@@ -663,18 +667,36 @@
|
||||
};
|
||||
|
||||
websocket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'init') {
|
||||
setLogs(data.data.logs || []);
|
||||
setLogs((data.data.logs || []).filter(log => log && log.timestamp));
|
||||
setStatus(data.data.status || status);
|
||||
setAccounts(data.data.accounts || []);
|
||||
setAccounts((data.data.accounts || []).filter(acc => acc && acc.email));
|
||||
} else if (data.type === 'log') {
|
||||
setLogs(prev => [...prev.slice(-99), data.log]);
|
||||
if (data.log && data.log.timestamp) {
|
||||
setLogs(prev => [...prev.slice(-99), data.log].filter(log => log && log.timestamp));
|
||||
}
|
||||
} else if (data.type === 'status') {
|
||||
setStatus(data.data);
|
||||
} else if (data.type === 'accounts') {
|
||||
setAccounts(data.data);
|
||||
setAccounts((data.data || []).filter(acc => acc && acc.email));
|
||||
} else if (data.type === 'account_update') {
|
||||
if (data.data && data.data.email) {
|
||||
setAccounts(prev => {
|
||||
const index = prev.findIndex(acc => acc.email === data.data.email);
|
||||
if (index >= 0) {
|
||||
const updated = [...prev];
|
||||
updated[index] = data.data;
|
||||
return updated;
|
||||
}
|
||||
return [...prev, data.data];
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -700,43 +722,61 @@
|
||||
|
||||
const startBot = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/start', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
showToast('Starting bot...', 'info')
|
||||
const res = await fetch('/api/start', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
showToast('Bot started successfully');
|
||||
fetchData();
|
||||
showToast(`Bot started successfully! (PID: ${data.pid})`)
|
||||
setTimeout(fetchData, 1000)
|
||||
} else {
|
||||
showToast(data.error || 'Failed to start bot', 'error');
|
||||
showToast(data.error || 'Failed to start bot', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to start bot', 'error');
|
||||
showToast('Failed to start bot: ' + error.message, 'error')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopBot = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/stop', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
showToast('Stopping bot...', 'info')
|
||||
const res = await fetch('/api/stop', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
showToast('Bot stop requested');
|
||||
fetchData();
|
||||
showToast('Bot stopped successfully')
|
||||
setTimeout(fetchData, 1000)
|
||||
} else {
|
||||
showToast(data.error || 'Failed to stop bot', 'error');
|
||||
showToast(data.error || 'Failed to stop bot', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to stop bot', 'error');
|
||||
showToast('Failed to stop bot: ' + error.message, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const restartBot = async () => {
|
||||
try {
|
||||
showToast('Restarting bot...', 'info')
|
||||
const res = await fetch('/api/restart', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
showToast(`Bot restarted successfully! (PID: ${data.pid})`)
|
||||
setTimeout(fetchData, 1000)
|
||||
} else {
|
||||
showToast(data.error || 'Failed to restart bot', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to restart bot: ' + error.message, 'error')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearLogs = async () => {
|
||||
try {
|
||||
await fetch('/api/logs', { method: 'DELETE' });
|
||||
setLogs([]);
|
||||
showToast('Logs cleared');
|
||||
await fetch('/api/logs', { method: 'DELETE' })
|
||||
setLogs([])
|
||||
showToast('Logs cleared')
|
||||
} catch (error) {
|
||||
showToast('Failed to clear logs', 'error');
|
||||
showToast('Failed to clear logs', 'error')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -837,6 +877,13 @@
|
||||
<i className="fas fa-stop"></i>
|
||||
Stop Bot
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={restartBot}
|
||||
>
|
||||
<i className="fas fa-redo"></i>
|
||||
Restart Bot
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={fetchData}
|
||||
@@ -913,7 +960,7 @@
|
||||
<i className="fas fa-stream"></i> No logs yet...
|
||||
</div>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
logs.filter(log => log && log.timestamp && log.level).map((log, index) => (
|
||||
<div key={index} className={`log-entry log-level-${log.level}`}>
|
||||
<span className="log-timestamp">
|
||||
[{new Date(log.timestamp).toLocaleTimeString()}]
|
||||
@@ -932,7 +979,11 @@
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div className={`toast toast-${toast.type}`}>
|
||||
<i className={`fas fa-${toast.type === 'success' ? 'check-circle' : 'exclamation-circle'}`}></i>
|
||||
<i className={`fas fa-${
|
||||
toast.type === 'success' ? 'check-circle' :
|
||||
toast.type === 'error' ? 'exclamation-circle' :
|
||||
'info-circle'
|
||||
}`}></i>
|
||||
<span>{toast.message}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -574,6 +574,10 @@
|
||||
border-left: 4px solid var(--danger);
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left: 4px solid var(--primary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.app-container {
|
||||
@@ -623,7 +627,7 @@
|
||||
|
||||
const showToast = (message, type = 'success') => {
|
||||
setToast({ message, type });
|
||||
setTimeout(() => setToast(null), 3000);
|
||||
setTimeout(() => setToast(null), 5000);
|
||||
};
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
@@ -718,43 +722,61 @@
|
||||
|
||||
const startBot = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/start', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
showToast('Starting bot...', 'info')
|
||||
const res = await fetch('/api/start', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
showToast('Bot started successfully');
|
||||
fetchData();
|
||||
showToast(`Bot started successfully! (PID: ${data.pid})`)
|
||||
setTimeout(fetchData, 1000)
|
||||
} else {
|
||||
showToast(data.error || 'Failed to start bot', 'error');
|
||||
showToast(data.error || 'Failed to start bot', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to start bot', 'error');
|
||||
showToast('Failed to start bot: ' + error.message, 'error')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopBot = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/stop', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
showToast('Stopping bot...', 'info')
|
||||
const res = await fetch('/api/stop', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
showToast('Bot stop requested');
|
||||
fetchData();
|
||||
showToast('Bot stopped successfully')
|
||||
setTimeout(fetchData, 1000)
|
||||
} else {
|
||||
showToast(data.error || 'Failed to stop bot', 'error');
|
||||
showToast(data.error || 'Failed to stop bot', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to stop bot', 'error');
|
||||
showToast('Failed to stop bot: ' + error.message, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const restartBot = async () => {
|
||||
try {
|
||||
showToast('Restarting bot...', 'info')
|
||||
const res = await fetch('/api/restart', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
showToast(`Bot restarted successfully! (PID: ${data.pid})`)
|
||||
setTimeout(fetchData, 1000)
|
||||
} else {
|
||||
showToast(data.error || 'Failed to restart bot', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to restart bot: ' + error.message, 'error')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearLogs = async () => {
|
||||
try {
|
||||
await fetch('/api/logs', { method: 'DELETE' });
|
||||
setLogs([]);
|
||||
showToast('Logs cleared');
|
||||
await fetch('/api/logs', { method: 'DELETE' })
|
||||
setLogs([])
|
||||
showToast('Logs cleared')
|
||||
} catch (error) {
|
||||
showToast('Failed to clear logs', 'error');
|
||||
showToast('Failed to clear logs', 'error')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -855,6 +877,13 @@
|
||||
<i className="fas fa-stop"></i>
|
||||
Stop Bot
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={restartBot}
|
||||
>
|
||||
<i className="fas fa-redo"></i>
|
||||
Restart Bot
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={fetchData}
|
||||
@@ -950,7 +979,11 @@
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div className={`toast toast-${toast.type}`}>
|
||||
<i className={`fas fa-${toast.type === 'success' ? 'check-circle' : 'exclamation-circle'}`}></i>
|
||||
<i className={`fas fa-${
|
||||
toast.type === 'success' ? 'check-circle' :
|
||||
toast.type === 'error' ? 'exclamation-circle' :
|
||||
'info-circle'
|
||||
}`}></i>
|
||||
<span>{toast.message}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
139
src/dashboard/BotController.ts
Normal file
139
src/dashboard/BotController.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { dashboardState } from './state'
|
||||
|
||||
// We'll import and run the bot directly in the same process
|
||||
let botRunning = false
|
||||
let botPromise: Promise<void> | null = null
|
||||
|
||||
export class BotController {
|
||||
private isRunning: boolean = false
|
||||
private startTime?: Date
|
||||
|
||||
constructor() {
|
||||
// Cleanup on exit
|
||||
process.on('exit', () => this.stop())
|
||||
}
|
||||
|
||||
private log(message: string, level: 'log' | 'warn' | 'error' = 'log'): void {
|
||||
console.log(`[BotController] ${message}`)
|
||||
|
||||
dashboardState.addLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
platform: 'MAIN',
|
||||
title: 'BOT-CONTROLLER',
|
||||
message
|
||||
})
|
||||
}
|
||||
|
||||
public async start(): Promise<{ success: boolean; error?: string; pid?: number }> {
|
||||
if (this.isRunning || botRunning) {
|
||||
return { success: false, error: 'Bot is already running' }
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('🚀 Starting bot...', 'log')
|
||||
|
||||
// Import the bot main logic
|
||||
const { MicrosoftRewardsBot } = await import('../index')
|
||||
|
||||
this.isRunning = true
|
||||
botRunning = true
|
||||
this.startTime = new Date()
|
||||
dashboardState.setRunning(true)
|
||||
|
||||
// Run the bot in the same process using the exact same logic as npm start
|
||||
botPromise = (async () => {
|
||||
try {
|
||||
const rewardsBot = new MicrosoftRewardsBot(false)
|
||||
|
||||
this.log('✓ Bot initialized, starting execution...', 'log')
|
||||
|
||||
await rewardsBot.initialize()
|
||||
await rewardsBot.run()
|
||||
|
||||
this.log('✓ Bot completed successfully', 'log')
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.log(`Bot error: ${errorMsg}`, 'error')
|
||||
throw error
|
||||
} finally {
|
||||
this.cleanup()
|
||||
}
|
||||
})()
|
||||
|
||||
// Don't await - let it run in background
|
||||
botPromise.catch(error => {
|
||||
this.log(`Bot execution failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
||||
})
|
||||
|
||||
return { success: true, pid: process.pid }
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.log(`Failed to start bot: ${errorMsg}`, 'error')
|
||||
this.cleanup()
|
||||
return { success: false, error: errorMsg }
|
||||
}
|
||||
}
|
||||
|
||||
public stop(): { success: boolean; error?: string } {
|
||||
if (!this.isRunning && !botRunning) {
|
||||
return { success: false, error: 'Bot is not running' }
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('🛑 Stopping bot...', 'warn')
|
||||
|
||||
// For now, we can't gracefully stop a running bot in the same process
|
||||
// This would require refactoring the bot to support cancellation
|
||||
this.log('⚠ Note: Bot will complete current task before stopping', 'warn')
|
||||
|
||||
this.cleanup()
|
||||
return { success: true }
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.log(`Error stopping bot: ${errorMsg}`, 'error')
|
||||
this.cleanup()
|
||||
return { success: false, error: errorMsg }
|
||||
}
|
||||
}
|
||||
|
||||
public async restart(): Promise<{ success: boolean; error?: string; pid?: number }> {
|
||||
this.log('🔄 Restarting bot...', 'log')
|
||||
this.stop()
|
||||
|
||||
// Wait a bit before restarting
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
const result = await this.start()
|
||||
resolve(result)
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
public getStatus(): {
|
||||
running: boolean
|
||||
pid?: number
|
||||
uptime?: number
|
||||
startTime?: string
|
||||
} {
|
||||
return {
|
||||
running: this.isRunning || botRunning,
|
||||
pid: process.pid,
|
||||
uptime: this.startTime ? Date.now() - this.startTime.getTime() : undefined,
|
||||
startTime: this.startTime?.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
this.isRunning = false
|
||||
botRunning = false
|
||||
botPromise = null
|
||||
this.startTime = undefined
|
||||
dashboardState.setRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const botController = new BotController()
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { spawn } from 'child_process'
|
||||
import { dashboardState } from './state'
|
||||
import { loadAccounts, loadConfig, getConfigPath } from '../util/Load'
|
||||
import { spawn } from 'child_process'
|
||||
import { botController } from './BotController'
|
||||
|
||||
export const apiRouter = Router()
|
||||
|
||||
@@ -144,46 +145,65 @@ apiRouter.post('/config', (req: Request, res: Response): void => {
|
||||
})
|
||||
|
||||
// POST /api/start - Start bot in background
|
||||
apiRouter.post('/start', (_req: Request, res: Response): void => {
|
||||
apiRouter.post('/start', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const status = dashboardState.getStatus()
|
||||
const status = botController.getStatus()
|
||||
if (status.running) {
|
||||
res.status(400).json({ error: 'Bot already running' })
|
||||
res.status(400).json({ error: 'Bot already running', pid: status.pid })
|
||||
return
|
||||
}
|
||||
|
||||
// Set running state
|
||||
dashboardState.setRunning(true)
|
||||
const result = await botController.start()
|
||||
|
||||
// Log the start
|
||||
dashboardState.addLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'log',
|
||||
platform: 'MAIN',
|
||||
title: 'DASHBOARD',
|
||||
message: 'Bot start requested from dashboard'
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Bot started successfully',
|
||||
pid: result.pid
|
||||
})
|
||||
|
||||
res.json({ success: true, message: 'Bot start requested. Check logs for progress.' })
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: result.error || 'Failed to start bot'
|
||||
})
|
||||
}
|
||||
} 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) => {
|
||||
apiRouter.post('/stop', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
dashboardState.setRunning(false)
|
||||
const result = botController.stop()
|
||||
|
||||
dashboardState.addLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'warn',
|
||||
platform: 'MAIN',
|
||||
title: 'DASHBOARD',
|
||||
message: 'Bot stop requested from dashboard'
|
||||
if (result.success) {
|
||||
res.json({ success: true, message: 'Bot stopped successfully' })
|
||||
} else {
|
||||
res.status(400).json({ success: false, error: result.error || 'Failed to stop bot' })
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
||||
}
|
||||
})
|
||||
|
||||
res.json({ success: true, message: 'Bot will stop after current task completes' })
|
||||
// POST /api/restart - Restart bot
|
||||
apiRouter.post('/restart', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const result = await botController.restart()
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Bot restarted successfully',
|
||||
pid: result.pid
|
||||
})
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: result.error || 'Failed to restart bot'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user