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:
2025-11-03 23:07:10 +01:00
parent 8e6e4f716f
commit ae4e34cd66
9 changed files with 2343 additions and 25 deletions

964
public/index.html Normal file
View File

@@ -0,0 +1,964 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Microsoft Rewards Bot - Dashboard</title>
<link rel="icon" type="image/png" href="/assets/logo.png">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root {
--primary: #0078d4;
--primary-dark: #005a9e;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--dark: #1e293b;
--darker: #0f172a;
--light: #f8fafc;
--gray: #64748b;
--border: #e2e8f0;
--shadow: rgba(0, 0, 0, 0.1);
--radius: 12px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: var(--dark);
overflow-x: hidden;
}
.app-container {
min-height: 100vh;
padding: 2rem;
max-width: 1600px;
margin: 0 auto;
}
/* Header */
.header {
background: white;
padding: 2rem;
border-radius: var(--radius);
box-shadow: 0 10px 30px var(--shadow);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
animation: slideDown 0.5s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.logo {
width: 50px;
height: 50px;
border-radius: 10px;
object-fit: cover;
}
.header-title {
font-size: 1.75rem;
font-weight: 700;
color: var(--dark);
margin-bottom: 0.25rem;
}
.header-subtitle {
font-size: 0.875rem;
color: var(--gray);
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 50px;
font-size: 0.875rem;
font-weight: 600;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.status-running {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.status-stopped {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: white;
animation: blink 1.5s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: var(--radius);
box-shadow: 0 4px 20px var(--shadow);
display: flex;
align-items: center;
gap: 1rem;
transition: transform 0.3s, box-shadow 0.3s;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 30px var(--shadow);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
flex-shrink: 0;
}
.stat-content {
flex: 1;
}
.stat-label {
font-size: 0.875rem;
color: var(--gray);
margin-bottom: 0.25rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--dark);
}
.stat-change {
font-size: 0.75rem;
color: var(--success);
margin-top: 0.25rem;
}
/* Card */
.card {
background: white;
padding: 1.5rem;
border-radius: var(--radius);
box-shadow: 0 4px 20px var(--shadow);
margin-bottom: 2rem;
animation: fadeIn 0.5s ease;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--border);
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--dark);
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title i {
color: var(--primary);
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-family: inherit;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
box-shadow: 0 4px 15px rgba(0, 120, 212, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 120, 212, 0.4);
}
.btn-danger {
background: linear-gradient(135deg, var(--danger), #dc2626);
color: white;
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
}
.btn-success {
background: linear-gradient(135deg, var(--success), #059669);
color: white;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.btn-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
/* Accounts */
.accounts-grid {
display: grid;
gap: 1rem;
}
.account-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border: 2px solid var(--border);
border-radius: 8px;
transition: all 0.3s;
}
.account-item:hover {
border-color: var(--primary);
box-shadow: 0 4px 15px var(--shadow);
}
.account-info {
display: flex;
align-items: center;
gap: 1rem;
}
.account-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
}
.account-details {
flex: 1;
}
.account-email {
font-weight: 600;
color: var(--dark);
margin-bottom: 0.25rem;
}
.account-status {
font-size: 0.75rem;
color: var(--gray);
}
.account-stats {
display: flex;
align-items: center;
gap: 2rem;
}
.account-points {
text-align: right;
}
.account-points-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
}
.account-points-label {
font-size: 0.75rem;
color: var(--gray);
}
.account-status-badge {
padding: 0.25rem 0.75rem;
border-radius: 50px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-idle {
background: #e2e8f0;
color: #64748b;
}
.status-running {
background: #dcfce7;
color: #16a34a;
}
.status-completed {
background: #dbeafe;
color: #2563eb;
}
.status-error {
background: #fee2e2;
color: #dc2626;
}
/* Logs */
.logs-container {
background: var(--darker);
border-radius: 8px;
padding: 1rem;
max-height: 500px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
.logs-container::-webkit-scrollbar {
width: 8px;
}
.logs-container::-webkit-scrollbar-track {
background: var(--dark);
border-radius: 4px;
}
.logs-container::-webkit-scrollbar-thumb {
background: var(--gray);
border-radius: 4px;
}
.log-entry {
padding: 0.5rem;
border-left: 3px solid transparent;
margin-bottom: 0.5rem;
border-radius: 4px;
transition: all 0.3s;
animation: logFadeIn 0.3s ease;
}
@keyframes logFadeIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.log-entry:hover {
background: rgba(255, 255, 255, 0.05);
}
.log-timestamp {
color: #64748b;
margin-right: 0.5rem;
}
.log-platform {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
margin-right: 0.5rem;
}
.platform-MAIN {
background: rgba(59, 130, 246, 0.2);
color: #60a5fa;
}
.platform-DESKTOP {
background: rgba(168, 85, 247, 0.2);
color: #c084fc;
}
.platform-MOBILE {
background: rgba(34, 197, 94, 0.2);
color: #4ade80;
}
.log-level-log {
border-left-color: #4ade80;
color: #d4d4d4;
}
.log-level-warn {
border-left-color: #fbbf24;
color: #fbbf24;
}
.log-level-error {
border-left-color: #ef4444;
color: #ef4444;
}
.log-title {
color: #94a3b8;
margin-right: 0.5rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem;
color: var(--gray);
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Loading */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
color: var(--gray);
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 1rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Toast */
.toast {
position: fixed;
bottom: 2rem;
right: 2rem;
background: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 10px 40px var(--shadow);
display: flex;
align-items: center;
gap: 1rem;
z-index: 1000;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(100px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.toast-success {
border-left: 4px solid var(--success);
}
.toast-error {
border-left: 4px solid var(--danger);
}
/* Responsive */
@media (max-width: 768px) {
.app-container {
padding: 1rem;
}
.header {
flex-direction: column;
gap: 1rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.account-item {
flex-direction: column;
gap: 1rem;
}
.account-stats {
width: 100%;
justify-content: space-between;
}
}
</style>
</head>
<body>
<div id="root"></div>
<!-- React & Babel -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect, useCallback } = React;
function App() {
const [status, setStatus] = useState({ running: false, totalAccounts: 0 });
const [accounts, setAccounts] = useState([]);
const [metrics, setMetrics] = useState({});
const [logs, setLogs] = useState([]);
const [ws, setWs] = useState(null);
const [loading, setLoading] = useState(true);
const [toast, setToast] = useState(null);
const showToast = (message, type = 'success') => {
setToast({ message, type });
setTimeout(() => setToast(null), 3000);
};
const fetchData = useCallback(async () => {
try {
const [statusRes, accountsRes, metricsRes] = await Promise.all([
fetch('/api/status'),
fetch('/api/accounts'),
fetch('/api/metrics')
]);
const statusData = await statusRes.json();
const accountsData = await accountsRes.json();
const metricsData = await metricsRes.json();
setStatus(statusData);
setAccounts(accountsData);
setMetrics(metricsData);
setLoading(false);
} catch (error) {
console.error('Failed to fetch data:', error);
showToast('Failed to fetch data', 'error');
}
}, []);
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 10000);
return () => clearInterval(interval);
}, [fetchData]);
useEffect(() => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const websocket = new WebSocket(`${protocol}//${window.location.host}`);
websocket.onopen = () => {
console.log('WebSocket connected');
};
websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'init') {
setLogs((data.data.logs || []).filter(log => log && log.timestamp));
setStatus(data.data.status || status);
setAccounts((data.data.accounts || []).filter(acc => acc && acc.email));
} else if (data.type === '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 || []).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);
}
};
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
websocket.onclose = () => {
console.log('WebSocket disconnected');
setTimeout(() => {
// Reconnect
const newWs = new WebSocket(`${protocol}//${window.location.host}`);
setWs(newWs);
}, 3000);
};
setWs(websocket);
return () => {
websocket.close();
};
}, []);
const startBot = async () => {
try {
const res = await fetch('/api/start', { method: 'POST' });
const data = await res.json();
if (data.success) {
showToast('Bot started successfully');
fetchData();
} else {
showToast(data.error || 'Failed to start bot', 'error');
}
} catch (error) {
showToast('Failed to start bot', 'error');
}
};
const stopBot = async () => {
try {
const res = await fetch('/api/stop', { method: 'POST' });
const data = await res.json();
if (data.success) {
showToast('Bot stop requested');
fetchData();
} else {
showToast(data.error || 'Failed to stop bot', 'error');
}
} catch (error) {
showToast('Failed to stop bot', 'error');
}
};
const clearLogs = async () => {
try {
await fetch('/api/logs', { method: 'DELETE' });
setLogs([]);
showToast('Logs cleared');
} catch (error) {
showToast('Failed to clear logs', 'error');
}
};
if (loading) {
return (
<div className="loading">
<div className="spinner"></div>
<span>Loading dashboard...</span>
</div>
);
}
return (
<div className="app-container">
{/* Header */}
<header className="header">
<div className="header-left">
<img src="/assets/logo.png" alt="Logo" className="logo" />
<div>
<h1 className="header-title">Microsoft Rewards Bot</h1>
<p className="header-subtitle">Real-time automation dashboard</p>
</div>
</div>
<div className={`status-badge ${status.running ? 'status-running' : 'status-stopped'}`}>
<div className="status-indicator"></div>
{status.running ? 'RUNNING' : 'STOPPED'}
</div>
</header>
{/* Stats */}
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon" style={{background: 'linear-gradient(135deg, #667eea, #764ba2)'}}>
<i className="fas fa-users"></i>
</div>
<div className="stat-content">
<div className="stat-label">Total Accounts</div>
<div className="stat-value">{metrics.totalAccounts || 0}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon" style={{background: 'linear-gradient(135deg, #f093fb, #f5576c)'}}>
<i className="fas fa-star"></i>
</div>
<div className="stat-content">
<div className="stat-label">Total Points</div>
<div className="stat-value">{(metrics.totalPoints || 0).toLocaleString()}</div>
{metrics.avgPoints > 0 && (
<div className="stat-change">
<i className="fas fa-chart-line"></i> Avg: {metrics.avgPoints}
</div>
)}
</div>
</div>
<div className="stat-card">
<div className="stat-icon" style={{background: 'linear-gradient(135deg, #4facfe, #00f2fe)'}}>
<i className="fas fa-check-circle"></i>
</div>
<div className="stat-content">
<div className="stat-label">Completed</div>
<div className="stat-value">{metrics.accountsCompleted || 0}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon" style={{background: 'linear-gradient(135deg, #fa709a, #fee140)'}}>
<i className="fas fa-exclamation-triangle"></i>
</div>
<div className="stat-content">
<div className="stat-label">Errors</div>
<div className="stat-value">{metrics.accountsWithErrors || 0}</div>
</div>
</div>
</div>
{/* Actions */}
<div className="card">
<div className="card-header">
<h2 className="card-title">
<i className="fas fa-bolt"></i>
Control Panel
</h2>
</div>
<div className="btn-group">
<button
className="btn btn-success"
onClick={startBot}
disabled={status.running}
>
<i className="fas fa-play"></i>
Start Bot
</button>
<button
className="btn btn-danger"
onClick={stopBot}
disabled={!status.running}
>
<i className="fas fa-stop"></i>
Stop Bot
</button>
<button
className="btn btn-primary"
onClick={fetchData}
>
<i className="fas fa-sync-alt"></i>
Refresh
</button>
<button
className="btn btn-primary"
onClick={clearLogs}
>
<i className="fas fa-trash"></i>
Clear Logs
</button>
</div>
</div>
{/* Accounts */}
<div className="card">
<div className="card-header">
<h2 className="card-title">
<i className="fas fa-user-friends"></i>
Accounts ({accounts.length})
</h2>
</div>
{accounts.length === 0 ? (
<div className="empty-state">
<i className="fas fa-inbox"></i>
<p>No accounts configured</p>
</div>
) : (
<div className="accounts-grid">
{accounts.map((account, index) => (
<div key={index} className="account-item">
<div className="account-info">
<div className="account-avatar">
{account.maskedEmail.charAt(0).toUpperCase()}
</div>
<div className="account-details">
<div className="account-email">{account.maskedEmail}</div>
<div className="account-status">
{account.lastSync ? `Last sync: ${new Date(account.lastSync).toLocaleString()}` : 'Never synced'}
</div>
</div>
</div>
<div className="account-stats">
<div className="account-points">
<div className="account-points-value">
{account.points !== undefined ? account.points.toLocaleString() : 'N/A'}
</div>
<div className="account-points-label">Points</div>
</div>
<span className={`account-status-badge status-${account.status}`}>
{account.status}
</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Logs */}
<div className="card">
<div className="card-header">
<h2 className="card-title">
<i className="fas fa-terminal"></i>
Live Logs ({logs.length})
</h2>
</div>
<div className="logs-container">
{logs.length === 0 ? (
<div style={{color: '#64748b', textAlign: 'center', padding: '2rem'}}>
<i className="fas fa-stream"></i> No logs yet...
</div>
) : (
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()}]
</span>
<span className={`log-platform platform-${log.platform}`}>
{log.platform}
</span>
<span className="log-title">[{log.title}]</span>
<span>{log.message}</span>
</div>
))
)}
</div>
</div>
{/* Toast */}
{toast && (
<div className={`toast toast-${toast.type}`}>
<i className={`fas fa-${toast.type === 'success' ? 'check-circle' : 'exclamation-circle'}`}></i>
<span>{toast.message}</span>
</div>
)}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>