ccsite-manager: fleet orchestrator, BTCPay auto-settlement, watchdog, dashboard
This commit is contained in:
257
templates/master.html
Normal file
257
templates/master.html
Normal file
@@ -0,0 +1,257 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Master Control — CCSite Fleet</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
:root{--bg:#080812;--card:#12121f;--accent:#6c63ff;--accent2:#00d4aa;--text:#e0e0e0;--muted:#777;--danger:#ef4444;--success:#22c55e;--warning:#f59e0b;--radius:10px}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||||
header{background:var(--card);border-bottom:1px solid rgba(108,99,255,0.2);padding:0.8rem 2rem;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:100}
|
||||
header h1{font-size:1.1rem;font-weight:800;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.tabs{display:flex;gap:0;border-bottom:1px solid rgba(255,255,255,0.06);background:var(--card);padding:0 2rem}
|
||||
.tab{padding:0.7rem 1.3rem;cursor:pointer;font-size:0.8rem;font-weight:600;color:var(--muted);border-bottom:2px solid transparent;transition:all 0.15s;text-transform:uppercase;letter-spacing:0.5px}
|
||||
.tab:hover{color:var(--text)}
|
||||
.tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||||
.container{max-width:1500px;margin:0 auto;padding:1.5rem}
|
||||
.stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:0.75rem;margin-bottom:1.5rem}
|
||||
.stat-card{background:var(--card);padding:1rem;border-radius:var(--radius);border:1px solid rgba(255,255,255,0.03);text-align:center}
|
||||
.stat-card .sv{font-size:1.4rem;font-weight:800;color:var(--accent2)}
|
||||
.stat-card .sl{font-size:0.6rem;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-top:0.15rem}
|
||||
.panel{display:none}.panel.active{display:block}
|
||||
table{width:100%;border-collapse:collapse;font-size:0.78rem}
|
||||
th,td{padding:0.45rem 0.6rem;text-align:left;border-bottom:1px solid rgba(255,255,255,0.04)}
|
||||
th{color:var(--muted);font-size:0.65rem;text-transform:uppercase;letter-spacing:0.5px;position:sticky;top:0;background:var(--bg)}
|
||||
tr:hover{background:rgba(108,99,255,0.03)}
|
||||
.badge{padding:0.15rem 0.45rem;border-radius:10px;font-size:0.6rem;font-weight:700}
|
||||
.badge-ok{background:rgba(34,197,94,0.15);color:var(--success)}
|
||||
.badge-pending{background:rgba(245,158,11,0.15);color:var(--warning)}
|
||||
.badge-danger{background:rgba(239,68,68,0.15);color:var(--danger)}
|
||||
input,select{padding:0.45rem 0.7rem;border-radius:6px;border:1px solid rgba(255,255,255,0.08);background:var(--card);color:var(--text);font-size:0.78rem;outline:none}
|
||||
input:focus,select:focus{border-color:var(--accent)}
|
||||
.search-bar{display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap}
|
||||
.btn{padding:0.4rem 0.9rem;border-radius:6px;border:none;cursor:pointer;font-weight:600;font-size:0.75rem;background:var(--accent);color:#fff}
|
||||
.btn:hover{filter:brightness(1.15)}
|
||||
.table-wrap{max-height:60vh;overflow-y:auto;border-radius:var(--radius);border:1px solid rgba(255,255,255,0.04)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🖥️ Master Control Panel</h1>
|
||||
<div style="font-size:0.7rem;color:var(--muted);"><a href="/" style="color:var(--accent);text-decoration:none;">← Dashboard</a> | <span id="clock">--</span></div>
|
||||
</header>
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchTab('overview')">📊 Overview</div>
|
||||
<div class="tab" onclick="switchTab('orders')">📦 Orders</div>
|
||||
<div class="tab" onclick="switchTab('users')">👥 Users</div>
|
||||
<div class="tab" onclick="switchTab('products')">🏷️ Products</div>
|
||||
<div class="tab" onclick="switchTab('transactions')">💳 Transactions</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div id="panel-overview" class="panel active">
|
||||
<div class="stat-row" id="overview-stats"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
|
||||
<div style="background:var(--card);padding:1rem;border-radius:var(--radius);">
|
||||
<h3 style="font-size:0.75rem;color:var(--muted);margin-bottom:0.8rem;">📦 Recent Orders</h3>
|
||||
<div id="recent-orders"></div>
|
||||
</div>
|
||||
<div style="background:var(--card);padding:1rem;border-radius:var(--radius);">
|
||||
<h3 style="font-size:0.75rem;color:var(--muted);margin-bottom:0.8rem;">👥 Recent Signups</h3>
|
||||
<div id="recent-users"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="panel-orders" class="panel">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="order-search" placeholder="Search orders..." oninput="renderOrders()">
|
||||
<select id="order-site" onchange="renderOrders()"><option value="">All Sites</option></select>
|
||||
<select id="order-status" onchange="renderOrders()"><option value="">All Status</option><option value="pending">Pending</option><option value="completed">Completed</option></select>
|
||||
</div>
|
||||
<div class="table-wrap"><table id="orders-table"><thead><tr><th>Site</th><th>Order</th><th>Product</th><th>Email</th><th>Amount</th><th>Status</th><th>Date</th></tr></thead><tbody></tbody></table></div>
|
||||
</div>
|
||||
<div id="panel-users" class="panel">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="user-search" placeholder="Search users..." oninput="renderUsers()">
|
||||
<select id="user-site" onchange="renderUsers()"><option value="">All Sites</option></select>
|
||||
</div>
|
||||
<div class="table-wrap"><table id="users-table"><thead><tr><th>Site</th><th>User</th><th>Email</th><th>Balance</th><th>Orders</th><th>Joined</th></tr></thead><tbody></tbody></table></div>
|
||||
</div>
|
||||
<div id="panel-products" class="panel">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="product-search" placeholder="Search products..." oninput="renderProducts()">
|
||||
<select id="product-site" onchange="renderProducts()"><option value="">All Sites</option></select>
|
||||
</div>
|
||||
<div class="table-wrap"><table id="products-table"><thead><tr><th>Site</th><th>Product</th><th>Category</th><th>Price</th><th>Stock</th></tr></thead><tbody></tbody></table></div>
|
||||
</div>
|
||||
<div id="panel-transactions" class="panel">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="txn-search" placeholder="Search transactions..." oninput="renderTransactions()">
|
||||
<select id="txn-site" onchange="renderTransactions()"><option value="">All Sites</option></select>
|
||||
</div>
|
||||
<div class="table-wrap"><table id="txns-table"><thead><tr><th>Site</th><th>User</th><th>Type</th><th>Method</th><th>Amount</th><th>Date</th></tr></thead><tbody></tbody></table></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const SITE_IPS = {1:'10.30.20.210',2:'10.30.20.211',3:'10.30.20.212',4:'10.30.20.213',5:'10.30.20.214',6:'10.30.20.217',7:'10.30.20.218',8:'10.30.20.219',9:'10.30.20.220',10:'10.30.20.221'};
|
||||
let ALL_DATA = {};
|
||||
|
||||
async function loadAll() {
|
||||
const promises = Object.entries(SITE_IPS).map(async ([id,ip]) => {
|
||||
try {
|
||||
const r = await fetch(`http://${ip}:5000/api/dashboard-data`);
|
||||
const d = await r.json();
|
||||
d._site_id = parseInt(id);
|
||||
d._ip = ip;
|
||||
return d;
|
||||
} catch(e) { return null; }
|
||||
});
|
||||
const results = await Promise.all(promises);
|
||||
ALL_DATA = results.filter(r => r);
|
||||
|
||||
// Populate site dropdowns
|
||||
const siteOpts = ALL_DATA.map(d => `<option value="${d._site_id}">${d.site}</option>`).join('');
|
||||
['order-site','user-site','product-site','txn-site'].forEach(id => {
|
||||
document.getElementById(id).innerHTML = '<option value="">All Sites</option>' + siteOpts;
|
||||
});
|
||||
|
||||
updateOverview();
|
||||
}
|
||||
|
||||
function updateOverview() {
|
||||
let totalOrders=0, totalUsers=0, totalRevenue=0, pendingOrders=0;
|
||||
ALL_DATA.forEach(d => {
|
||||
totalOrders += d.orders.length;
|
||||
totalUsers += d.users.length;
|
||||
totalRevenue += d.orders.filter(o=>o.status==='completed').reduce((s,o)=>s+o.amount,0);
|
||||
pendingOrders += d.orders.filter(o=>o.status==='pending').length;
|
||||
});
|
||||
|
||||
document.getElementById('overview-stats').innerHTML = `
|
||||
<div class="stat-card"><div class="sv">${totalUsers}</div><div class="sl">Total Users</div></div>
|
||||
<div class="stat-card"><div class="sv">${totalOrders}</div><div class="sl">Total Orders</div></div>
|
||||
<div class="stat-card"><div class="sv">$${totalRevenue.toFixed(0)}</div><div class="sl">Revenue</div></div>
|
||||
<div class="stat-card"><div class="sv" style="color:var(--warning);">${pendingOrders}</div><div class="sl">Pending</div></div>
|
||||
<div class="stat-card"><div class="sv">${ALL_DATA.length}</div><div class="sl">Sites Online</div></div>
|
||||
`;
|
||||
|
||||
// Recent orders
|
||||
const allOrders = ALL_DATA.flatMap(d => d.orders.map(o => ({...o, site:d.site, sid:d._site_id})));
|
||||
allOrders.sort((a,b) => b.date.localeCompare(a.date));
|
||||
document.getElementById('recent-orders').innerHTML = allOrders.slice(0,10).map(o =>
|
||||
`<div style="padding:0.3rem 0;border-bottom:1px solid rgba(255,255,255,0.03);font-size:0.75rem;">
|
||||
<span style="color:var(--accent);">${o.site}</span> · ${o.product?.slice(0,30)} ·
|
||||
<span style="color:var(--accent2);">$${o.amount?.toFixed(2)}</span>
|
||||
<span class="badge ${o.status==='completed'?'badge-ok':'badge-pending'}">${o.status}</span>
|
||||
</div>`
|
||||
).join('') || '<div style="color:var(--muted);">No orders yet</div>';
|
||||
|
||||
// Recent users
|
||||
const allUsers = ALL_DATA.flatMap(d => d.users.map(u => ({...u, site:d.site})));
|
||||
allUsers.sort((a,b) => b.joined.localeCompare(a.joined));
|
||||
document.getElementById('recent-users').innerHTML = allUsers.slice(0,10).map(u =>
|
||||
`<div style="padding:0.3rem 0;border-bottom:1px solid rgba(255,255,255,0.03);font-size:0.75rem;">
|
||||
<span style="color:var(--accent);">${u.site}</span> · ${u.username} · ${u.email}
|
||||
</div>`
|
||||
).join('') || '<div style="color:var(--muted);">No users yet</div>';
|
||||
}
|
||||
|
||||
function renderOrders() {
|
||||
const q = (document.getElementById('order-search').value||'').toLowerCase();
|
||||
const sid = parseInt(document.getElementById('order-site').value)||0;
|
||||
const status = document.getElementById('order-status').value;
|
||||
|
||||
let orders = ALL_DATA.flatMap(d => d.orders.map(o => ({...o, site:d.site, sid:d._site_id})));
|
||||
if(sid) orders = orders.filter(o => o.sid===sid);
|
||||
if(status) orders = orders.filter(o => o.status===status);
|
||||
if(q) orders = orders.filter(o => (o.product+o.email+o.code).toLowerCase().includes(q));
|
||||
orders.sort((a,b) => b.date.localeCompare(a.date));
|
||||
|
||||
document.getElementById('orders-table').querySelector('tbody').innerHTML = orders.slice(0,200).map(o =>
|
||||
`<tr>
|
||||
<td style="color:var(--accent);">${o.site}</td>
|
||||
<td>#${o.id} <span style="font-size:0.65rem;color:var(--muted);">${o.code||''}</span></td>
|
||||
<td>${o.product?.slice(0,35)}</td>
|
||||
<td>${o.email}</td>
|
||||
<td style="color:var(--accent2);">$${o.amount?.toFixed(2)}</td>
|
||||
<td><span class="badge ${o.status==='completed'?'badge-ok':'badge-pending'}">${o.status}</span></td>
|
||||
<td style="font-size:0.7rem;color:var(--muted);">${new Date(o.date).toLocaleDateString()}</td>
|
||||
</tr>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
const q = (document.getElementById('user-search').value||'').toLowerCase();
|
||||
const sid = parseInt(document.getElementById('user-site').value)||0;
|
||||
let users = ALL_DATA.flatMap(d => d.users.map(u => ({...u, site:d.site, sid:d._site_id})));
|
||||
if(sid) users = users.filter(u => u.sid===sid);
|
||||
if(q) users = users.filter(u => (u.username+u.email).toLowerCase().includes(q));
|
||||
|
||||
document.getElementById('users-table').querySelector('tbody').innerHTML = users.slice(0,200).map(u =>
|
||||
`<tr>
|
||||
<td style="color:var(--accent);">${u.site}</td>
|
||||
<td><strong>${u.username}</strong></td>
|
||||
<td>${u.email}</td>
|
||||
<td style="color:var(--accent2);">$${u.balance?.toFixed(2)}</td>
|
||||
<td>${u.orders}</td>
|
||||
<td style="font-size:0.7rem;color:var(--muted);">${new Date(u.joined).toLocaleDateString()}</td>
|
||||
</tr>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function renderProducts() {
|
||||
const q = (document.getElementById('product-search').value||'').toLowerCase();
|
||||
const sid = parseInt(document.getElementById('product-site').value)||0;
|
||||
let prods = ALL_DATA.flatMap(d => d.products.map(p => ({...p, site:d.site, sid:d._site_id})));
|
||||
if(sid) prods = prods.filter(p => p.sid===sid);
|
||||
if(q) prods = prods.filter(p => p.name.toLowerCase().includes(q));
|
||||
|
||||
document.getElementById('products-table').querySelector('tbody').innerHTML = prods.slice(0,200).map(p =>
|
||||
`<tr>
|
||||
<td style="color:var(--accent);">${p.site}</td>
|
||||
<td>${p.name}</td>
|
||||
<td>${p.category}</td>
|
||||
<td style="color:var(--accent2);">$${p.price?.toFixed(2)}</td>
|
||||
<td>${p.stock}</td>
|
||||
</tr>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function renderTransactions() {
|
||||
const q = (document.getElementById('txn-search').value||'').toLowerCase();
|
||||
const sid = parseInt(document.getElementById('txn-site').value)||0;
|
||||
let txns = ALL_DATA.flatMap(d => d.transactions.map(t => ({...t, site:d.site, sid:d._site_id})));
|
||||
if(sid) txns = txns.filter(t => t.sid===sid);
|
||||
if(q) txns = txns.filter(t => (t.user+t.method).toLowerCase().includes(q));
|
||||
|
||||
document.getElementById('txns-table').querySelector('tbody').innerHTML = txns.slice(0,200).map(t =>
|
||||
`<tr>
|
||||
<td style="color:var(--accent);">${t.site}</td>
|
||||
<td>${t.user}</td>
|
||||
<td><span class="badge ${t.type==='deposit'?'badge-ok':'badge-danger'}">${t.type}</span></td>
|
||||
<td>${t.method||'-'}</td>
|
||||
<td style="color:${t.amount>0?'var(--success)':'var(--danger)'};">${t.amount>0?'+':''}$${t.amount?.toFixed(2)}</td>
|
||||
<td style="font-size:0.7rem;color:var(--muted);">${new Date(t.date).toLocaleDateString()}</td>
|
||||
</tr>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||
const tabMap = {'overview':1,'orders':2,'users':3,'products':4,'transactions':5};
|
||||
document.querySelector('.tab:nth-child(' + tabMap[tab] + ')').classList.add('active');
|
||||
document.getElementById('panel-'+tab).classList.add('active');
|
||||
if(tab==='orders') renderOrders();
|
||||
if(tab==='users') renderUsers();
|
||||
if(tab==='products') renderProducts();
|
||||
if(tab==='transactions') renderTransactions();
|
||||
}
|
||||
|
||||
setInterval(() => document.getElementById('clock').textContent = new Date().toLocaleTimeString(), 1000);
|
||||
loadAll();
|
||||
setInterval(loadAll, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user