add simple ui
This commit is contained in:
parent
b01f418c07
commit
b700fd01a4
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ node_modules/
|
|||||||
.wwebjs_auth/
|
.wwebjs_auth/
|
||||||
.wwebjs_cache/
|
.wwebjs_cache/
|
||||||
*.session
|
*.session
|
||||||
|
.idea/
|
||||||
155
index.js
155
index.js
@ -16,6 +16,11 @@ let starting = false;
|
|||||||
let restarting = false;
|
let restarting = false;
|
||||||
let msgCounter = 0;
|
let msgCounter = 0;
|
||||||
|
|
||||||
|
let startTime = Date.now();
|
||||||
|
let totalForwarded = 0;
|
||||||
|
let userStats = {};
|
||||||
|
let groupStats = {};
|
||||||
|
|
||||||
function ts() {
|
function ts() {
|
||||||
return new Date().toLocaleString('he-IL', { hour12: false });
|
return new Date().toLocaleString('he-IL', { hour12: false });
|
||||||
}
|
}
|
||||||
@ -76,6 +81,11 @@ async function flushQueue() {
|
|||||||
try {
|
try {
|
||||||
await sendSMS(text);
|
await sendSMS(text);
|
||||||
log('INFO', `Flushed ${batch.length} messages`);
|
log('INFO', `Flushed ${batch.length} messages`);
|
||||||
|
for (const m of batch) {
|
||||||
|
userStats[m.sender] = (userStats[m.sender] || 0) + 1;
|
||||||
|
groupStats[m.group] = (groupStats[m.group] || 0) + 1;
|
||||||
|
totalForwarded++;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('ERROR', `Flush failed: ${err.message}`);
|
log('ERROR', `Flush failed: ${err.message}`);
|
||||||
messageQueue = batch.concat(messageQueue);
|
messageQueue = batch.concat(messageQueue);
|
||||||
@ -256,12 +266,149 @@ async function startClient() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const http = require('http');
|
||||||
* 🔥 FIX: detect detached frame / WhatsApp Web crashes
|
function renderDashboard() {
|
||||||
*/
|
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
||||||
|
const h = Math.floor(uptime / 3600);
|
||||||
|
const m = Math.floor((uptime % 3600) / 60);
|
||||||
|
const s = uptime % 60;
|
||||||
|
const uptimeStr = `${h}h ${m}m ${s}s`;
|
||||||
|
|
||||||
|
const userRows = Object.entries(userStats)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([name, count]) =>
|
||||||
|
`<tr><td>${name}</td><td>${count}</td></tr>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const groupRows = Object.entries(groupStats)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([name, count]) =>
|
||||||
|
`<tr><td>${name}</td><td>${count}</td></tr>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const connected = client && !restarting;
|
||||||
|
|
||||||
|
const api = JSON.stringify({
|
||||||
|
uptime, uptimeStr, connected,
|
||||||
|
totalForwarded, queued: messageQueue.length, flushTime: flushTime(),
|
||||||
|
userStats, groupStats,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OmegaBaSMS</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a; color: #e2e8f0; min-height: 100vh; padding: 2rem;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.75rem; font-weight: 700; margin-bottom: 0.25rem;
|
||||||
|
background: linear-gradient(135deg, #22d3ee, #3b82f6);
|
||||||
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
.subtitle { color: #64748b; margin-bottom: 2rem; font-size: 0.9rem; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
||||||
|
.card {
|
||||||
|
background: #1e293b; border-radius: 12px; padding: 1.25rem; border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
.card .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; margin-bottom: 0.5rem; }
|
||||||
|
.card .value { font-size: 1.75rem; font-weight: 700; }
|
||||||
|
.card .value.green { color: #22c55e; }
|
||||||
|
.card .value.blue { color: #3b82f6; }
|
||||||
|
.card .value.yellow { color: #eab308; }
|
||||||
|
.card .value.red { color: #ef4444; }
|
||||||
|
.status-dot {
|
||||||
|
display: inline-block; width: 10px; height: 10px; border-radius: 50%;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.status-dot.on { background: #22c55e; box-shadow: 0 0 8px #22c55e88; }
|
||||||
|
.status-dot.off { background: #ef4444; box-shadow: 0 0 8px #ef444488; }
|
||||||
|
.tables { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
|
||||||
|
@media (max-width: 640px) { .tables { grid-template-columns: 1fr; } }
|
||||||
|
h2 { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; color: #94a3b8; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th { text-align: left; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; padding: 0.5rem 0.75rem; border-bottom: 1px solid #334155; }
|
||||||
|
td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #1e293b; font-size: 0.9rem; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
td:last-child { text-align: right; font-weight: 600; }
|
||||||
|
.bar-bg { background: #334155; border-radius: 4px; height: 6px; overflow: hidden; margin-top: 4px; }
|
||||||
|
.bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, #3b82f6, #22d3ee); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<h1>OmegaBaSMS</h1>
|
||||||
|
<p class="subtitle">WhatsApp group messages forwarded via SMS</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card"><div class="label">Status</div><div class="value" id="status">—</div></div>
|
||||||
|
<div class="card"><div class="label">Uptime</div><div class="value blue" id="uptime">—</div></div>
|
||||||
|
<div class="card"><div class="label">Forwarded</div><div class="value green" id="forwarded">—</div></div>
|
||||||
|
<div class="card"><div class="label">Queued</div><div class="value yellow" id="queued">—</div><div style="font-size:0.75rem;color:#64748b;margin-top:0.25rem" id="flushTime"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="tables">
|
||||||
|
<div><h2>By Sender</h2><table><thead><tr><th>Name</th><th>Messages</th></tr></thead><tbody id="users"></tbody></table></div>
|
||||||
|
<div><h2>By Group</h2><table><thead><tr><th>Group</th><th>Messages</th></tr></thead><tbody id="groups"></tbody></table></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function poll() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/stats');
|
||||||
|
const d = await r.json();
|
||||||
|
document.getElementById('status').innerHTML = '<span class="status-dot ' + (d.connected ? 'on' : 'off') + '"></span>' + (d.connected ? 'Connected' : 'Disconnected');
|
||||||
|
document.getElementById('uptime').textContent = d.uptimeStr;
|
||||||
|
document.getElementById('forwarded').textContent = d.totalForwarded;
|
||||||
|
document.getElementById('queued').textContent = d.queued;
|
||||||
|
document.getElementById('flushTime').textContent = d.queued > 0 ? 'flushed at ' + d.flushTime : '';
|
||||||
|
document.getElementById('users').innerHTML = Object.entries(d.userStats).sort((a,b) => b[1]-a[1]).map(([n,c]) => '<tr><td>' + n + '</td><td>' + c + '</td></tr>').join('') || '<tr><td colspan="2" style="color:#64748b;">No messages yet</td></tr>';
|
||||||
|
document.getElementById('groups').innerHTML = Object.entries(d.groupStats).sort((a,b) => b[1]-a[1]).map(([n,c]) => '<tr><td>' + n + '</td><td>' + c + '</td></tr>').join('') || '<tr><td colspan="2" style="color:#64748b;">No messages yet</td></tr>';
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
setInterval(poll, 3000);
|
||||||
|
poll();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url === '/liveness') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('OK');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.url === '/api/stats') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
uptime: Math.floor((Date.now() - startTime) / 1000),
|
||||||
|
uptimeStr: (() => {
|
||||||
|
const t = Math.floor((Date.now() - startTime) / 1000);
|
||||||
|
return Math.floor(t/3600)+'h '+Math.floor((t%3600)/60)+'m '+t%60+'s';
|
||||||
|
})(),
|
||||||
|
connected: !!(client && !restarting),
|
||||||
|
totalForwarded,
|
||||||
|
queued: messageQueue.length,
|
||||||
|
flushTime: flushTime(),
|
||||||
|
userStats,
|
||||||
|
groupStats,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
|
res.end(renderDashboard());
|
||||||
|
});
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
server.listen(PORT, () => log('INIT', `Dashboard on http://0.0.0.0:${PORT}`));
|
||||||
|
|
||||||
|
/** Recoverable error detection */
|
||||||
function shouldRestart(err) {
|
function shouldRestart(err) {
|
||||||
const msg = (err && err.message) || String(err);
|
const msg = (err && err.message) || String(err);
|
||||||
return msg.includes('detached Frame') ||
|
return msg.includes('detached') ||
|
||||||
msg.includes('Execution context was destroyed') ||
|
msg.includes('Execution context was destroyed') ||
|
||||||
msg.includes('Target closed') ||
|
msg.includes('Target closed') ||
|
||||||
msg.includes('Session closed') ||
|
msg.includes('Session closed') ||
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -2573,4 +2573,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user