38753-vm/index.php
2026-02-25 00:03:44 +00:00

588 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>๐Œ๐จ๐ง๐ข๐ญ๐จ๐ซ ๐”๐ฉ๐ญ๐ข๐ฆ๐ž ๐›๐ฒ ๐˜๐ฎ๐ฆ๐ž๐ž</title>
<link rel="manifest" href="manifest.json">
<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=JetBrains+Mono:wght@400;700&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--crypto-green: #00ff88;
--crypto-red: #ff3366;
--crypto-blue: #00d4ff;
--dark-bg: #0b0e11;
--dark-card: #181a20;
--dark-sidebar: #1e2329;
--text-main: #eaecef;
--text-dim: #848e9c;
--neon-glow: 0 0 10px rgba(0, 255, 136, 0.5);
--border: #2b3139;
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 0;
font-family: 'Inter', sans-serif;
background: var(--dark-bg);
color: var(--text-main);
display: flex;
min-height: 100vh;
overflow-x: hidden;
}
/* Sidebar */
aside {
width: 260px;
background: var(--dark-sidebar);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
position: sticky;
top: 0;
height: 100vh;
z-index: 100;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid var(--border);
}
.sidebar-header h1 {
font-size: 1.1rem;
margin: 0;
background: linear-gradient(90deg, #fff, var(--crypto-blue));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800;
}
.nav-list { list-style: none; padding: 1rem 0; margin: 0; flex: 1; }
.nav-item {
padding: 0.8rem 1.5rem;
cursor: pointer;
color: var(--text-dim);
font-size: 0.9rem;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 12px;
}
.nav-item:hover, .nav-item.active {
color: #fff;
background: rgba(255,255,255,0.05);
border-left: 3px solid var(--crypto-blue);
}
/* Main Content */
main {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.top-bar {
height: 60px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2rem;
background: var(--dark-bg);
position: sticky;
top: 0;
z-index: 99;
}
.content-area { padding: 2rem; max-width: 1400px; width: 100%; margin: 0 auto; }
/* Cards & Components */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 2rem;
}
.card {
background: var(--dark-card);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--border);
position: relative;
}
.stat-val { font-family: 'JetBrains Mono', monospace; font-size: 1.8rem; font-weight: 700; display: block; margin-top: 5px; }
.stat-label { color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; }
/* Battery Bars */
.battery-container { display: flex; gap: 2px; height: 20px; margin-top: 10px; }
.battery-bar { flex: 1; background: #2b3139; border-radius: 2px; position: relative; }
.battery-bar.ok { background: var(--crypto-green); box-shadow: 0 0 5px rgba(0, 255, 136, 0.3); }
.battery-bar.err { background: var(--crypto-red); }
/* Mobile Monitor Specific (CRYPTO STYLE) */
#mobile-monitor {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: #000;
z-index: 2000;
display: none;
flex-direction: column;
padding: 5px;
}
#mobile-monitor.active { display: flex; }
.mm-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #111;
padding: 8px 12px;
border-radius: 8px;
margin-bottom: 5px;
}
.mm-grid {
display: grid;
grid-template-columns: 1fr;
gap: 5px;
flex: 1;
overflow-y: auto;
}
.mm-row {
background: #080808;
padding: 10px;
border-radius: 8px;
border: 1px solid #1a1a1a;
display: flex;
flex-direction: column;
gap: 5px;
}
.mm-info {
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'JetBrains Mono';
}
.mm-url-text { font-size: 0.6rem; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 60%; }
.mm-lat-text { font-size: 0.7rem; font-weight: bold; }
.mm-spark-container {
height: 60px;
width: 100%;
position: relative;
background: rgba(255,255,255,0.02);
border-radius: 4px;
overflow: hidden;
}
.mm-canvas {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
}
/* List Tables */
.data-table { width: 100%; border-collapse: collapse; }
.data-table th { text-align: left; color: var(--text-dim); padding: 12px; border-bottom: 1px solid var(--border); font-size: 0.8rem; }
.data-table td { padding: 12px; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
.status-badge { padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; text-transform: uppercase; }
.bg-ok { background: rgba(0, 255, 136, 0.1); color: var(--crypto-green); }
.bg-err { background: rgba(255, 51, 102, 0.1); color: var(--crypto-red); }
/* Inputs & Buttons */
input, select {
background: #1e2329;
border: 1px solid var(--border);
color: #fff;
padding: 10px 12px;
border-radius: 4px;
outline: none;
width: 100%;
}
button {
padding: 10px 20px;
border-radius: 4px;
border: none;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.btn-glow { background: var(--crypto-blue); color: #000; box-shadow: 0 0 15px rgba(0, 212, 255, 0.2); }
/* Sections */
.section { display: none; }
.section.active { display: block; }
/* Responsive */
@media (max-width: 768px) {
aside { display: none; }
.top-bar { padding: 0 1rem; }
.content-area { padding: 1rem; }
.dashboard-grid { grid-template-columns: 1fr !important; }
}
</style>
</head>
<body>
<aside>
<div class="sidebar-header">
<h1>๐Œ๐จ๐ง๐ข๐ญ๐จ๐ซ ๐”๐ฉ๐ญ๐ข๐ฆ๐ž</h1>
<div style="font-size: 0.6rem; color: var(--text-dim); margin-top: 4px;">by Yumee โ€ข Healthcare AI</div>
</div>
<ul class="nav-list">
<li class="nav-item active" onclick="showSection('dashboard')"><span>๐Ÿ“Š</span> Dashboard</li>
<li class="nav-item" onclick="showSection('monitors')"><span>๐Ÿ”</span> Monitors</li>
<li class="nav-item" onclick="showSection('teams')"><span>๐Ÿ‘ฅ</span> Team</li>
<li class="nav-item" onclick="showSection('api')"><span>๐Ÿ”‘</span> API</li>
</ul>
<div style="padding: 1rem; border-top: 1px solid var(--border);">
<button class="btn-glow" style="width:100%; margin-bottom:10px;" onclick="enterMobileMonitor()">๐Ÿ“ฑ CRYPTO MODE</button>
<button id="global-toggle" style="width:100%; font-size: 0.8rem;" onclick="toggleMonitoring()">STATION: OFFLINE</button>
</div>
</aside>
<main>
<div class="top-bar">
<div id="ticker" style="font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--crypto-green);">
[NETWORK HEALTH: 100%] [NODES ACTIVE: 12]
</div>
<div style="display:flex; align-items:center; gap: 15px;">
<span id="live-indicator" style="width:10px; height:10px; border-radius:50%; background: #555;"></span>
<span id="clock" style="font-family:'JetBrains Mono'; font-size:0.8rem;">00:00:00</span>
</div>
</div>
<div class="content-area">
<div id="dashboard" class="section active">
<div class="dashboard-grid">
<div class="card" onclick="enterMobileMonitor()" style="cursor:pointer">
<span class="stat-label">Total Assets</span>
<span class="stat-val" id="stat-total">0</span>
</div>
<div class="card">
<span class="stat-label">Avg Latency</span>
<span class="stat-val" id="stat-latency" style="color: var(--crypto-blue)">0ms</span>
</div>
<div class="card">
<span class="stat-label">Health</span>
<span class="stat-val" id="stat-health" style="color: var(--crypto-green)">100%</span>
</div>
</div>
<div class="card" style="margin-bottom: 2rem;">
<h3>Asset Status (Battery View)</h3>
<div id="battery-rows"></div>
</div>
<div class="card">
<h3>Latency Graph</h3>
<canvas id="globalChart" style="height: 200px;"></canvas>
</div>
</div>
<div id="monitors" class="section">
<div class="card" style="margin-bottom: 20px;">
<h3>Add Asset</h3>
<div style="display:flex; gap: 10px;">
<input type="url" id="new-url" placeholder="https://example.com">
<button class="btn-glow" onclick="addAsset()">+ ADD</button>
</div>
</div>
<div class="card">
<table class="data-table">
<thead><tr><th>URL</th><th>Status</th><th>Latency</th><th>Actions</th></tr></thead>
<tbody id="inventory-list"></tbody>
</table>
</div>
</div>
<div id="teams" class="section"><div class="card"><h3>Team Management</h3><p>Manage your monitoring teams here.</p></div></div>
<div id="api" class="section"><div class="card"><h3>API Keys</h3><p>Generate keys for external integrations.</p></div></div>
</div>
</main>
<!-- MOBILE MONITOR OVERLAY (CRYPTO STYLE) -->
<div id="mobile-monitor">
<div class="mm-header">
<div style="display:flex; align-items:center;">
<div id="mm-status-dot" style="width:8px; height:8px; border-radius:50%; background:#555; margin-right:8px;"></div>
<span style="font-weight:bold; font-size:0.7rem; color:#fff; font-family:'JetBrains Mono'">REALTIME MONITOR</span>
</div>
<div style="display:flex; gap:5px;">
<button onclick="toggleFullscreen()" style="background:#1a1a1a; color:#888; border:1px solid #333; padding:4px 8px; font-size:0.5rem; border-radius:4px;">FS</button>
<button onclick="exitMobileMonitor()" style="background:#300; color:#f55; border:1px solid #500; padding:4px 8px; font-size:0.5rem; border-radius:4px;">EXIT</button>
</div>
</div>
<div id="mm-crypto-grid" class="mm-grid"></div>
<div style="background:#111; padding:8px 12px; border-radius:8px; margin-top:5px; display:flex; justify-content:space-between; align-items:center;">
<div id="mm-clock" style="font-family:'JetBrains Mono'; font-size:0.65rem; color:var(--crypto-blue)">00:00:00</div>
<div id="mm-health-pct" style="font-family:'JetBrains Mono'; font-size:0.65rem; color:var(--crypto-green)">100% HEALTH</div>
</div>
</div>
<script>
let isMonitoring = false;
let mainChart = null;
const sparklines = {};
class CryptoSparkline {
constructor(canvasId, initialData = []) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.data = initialData.length ? initialData : Array(30).fill(0);
this.color = '#00ff88';
this.resize();
window.addEventListener('resize', () => this.resize());
}
resize() {
const rect = this.canvas.parentElement.getBoundingClientRect();
this.canvas.width = rect.width * window.devicePixelRatio;
this.canvas.height = rect.height * window.devicePixelRatio;
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
this.draw();
}
update(val, status) {
this.data.push(val);
if (this.data.length > 50) this.data.shift();
this.color = status === 'ok' ? '#00ff88' : '#ff3366';
this.draw();
}
draw() {
const w = this.canvas.width / window.devicePixelRatio;
const h = this.canvas.height / window.devicePixelRatio;
this.ctx.clearRect(0, 0, w, h);
if (this.data.length < 2) return;
const min = Math.min(...this.data) * 0.8;
const max = Math.max(...this.data) * 1.2 || 100;
const range = max - min;
this.ctx.beginPath();
this.ctx.strokeStyle = this.color;
this.ctx.lineWidth = 2;
this.ctx.lineJoin = 'round';
this.ctx.lineCap = 'round';
const step = w / (this.data.length - 1);
for (let i = 0; i < this.data.length; i++) {
const x = i * step;
const y = h - ((this.data[i] - min) / range * h);
if (i === 0) this.ctx.moveTo(x, y);
else this.ctx.lineTo(x, y);
}
this.ctx.stroke();
// Gradient fill
const fillGrad = this.ctx.createLinearGradient(0, 0, 0, h);
fillGrad.addColorStop(0, this.color + '44');
fillGrad.addColorStop(1, this.color + '00');
this.ctx.lineTo(w, h);
this.ctx.lineTo(0, h);
this.ctx.fillStyle = fillGrad;
this.ctx.fill();
}
}
function showSection(id) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
const sec = document.getElementById(id);
if (sec) sec.classList.add('active');
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
refreshData();
}
setInterval(() => {
const time = new Date().toISOString().split('T')[1].split('.')[0];
document.getElementById('clock').innerText = time + ' UTC';
document.getElementById('mm-clock').innerText = time + ' UTC';
}, 1000);
function toggleFullscreen() {
if (!document.fullscreenElement) document.documentElement.requestFullscreen();
else document.exitFullscreen();
}
function enterMobileMonitor() {
document.getElementById('mobile-monitor').style.display = 'flex';
refreshData();
}
function exitMobileMonitor() {
document.getElementById('mobile-monitor').style.display = 'none';
if (document.fullscreenElement) document.exitFullscreen();
}
async function init() {
const resp = await fetch('api/uptime.php?action=settings');
const data = await resp.json();
isMonitoring = data.monitoring_enabled;
updateStatusUI();
initChart();
refreshData();
setInterval(refreshData, 10000);
setInterval(runPulse, 15000);
}
function initChart() {
const ctx = document.getElementById('globalChart').getContext('2d');
mainChart = new Chart(ctx, {
type: 'line',
data: {
labels: Array(20).fill(''),
datasets: [{
data: Array(20).fill(0),
borderColor: '#00d4ff',
backgroundColor: 'rgba(0, 212, 255, 0.05)',
fill: true, tension: 0.4, pointRadius: 0
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: { x: { display: false }, y: { grid: { color: '#2b3139' }, ticks: { color: '#848e9c', font: { size: 9 } } } },
plugins: { legend: { display: false } }
}
});
}
function updateStatusUI() {
const btn = document.getElementById('global-toggle');
const ind = document.getElementById('live-indicator');
const mmDot = document.getElementById('mm-status-dot');
const color = isMonitoring ? 'var(--crypto-green)' : '#555';
btn.innerText = isMonitoring ? 'STATION: ONLINE' : 'STATION: OFFLINE';
btn.style.background = isMonitoring ? 'var(--crypto-green)' : '#2b3139';
btn.style.color = isMonitoring ? '#000' : '#fff';
ind.style.background = color;
ind.style.boxShadow = isMonitoring ? '0 0 10px ' + color : 'none';
mmDot.style.background = color;
mmDot.style.boxShadow = isMonitoring ? '0 0 8px ' + color : 'none';
}
async function toggleMonitoring() {
isMonitoring = !isMonitoring;
await fetch('api/uptime.php?action=toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: isMonitoring })
});
updateStatusUI();
}
async function refreshData() {
const statResp = await fetch('api/uptime.php?action=stats');
const stats = await statResp.json();
document.getElementById('stat-total').innerText = stats.total;
document.getElementById('stat-latency').innerText = Math.round(stats.avg_latency) + 'ms';
const health = stats.total > 0 ? ((stats.up / stats.total) * 100).toFixed(1) : 100;
document.getElementById('stat-health').innerText = health + '%';
document.getElementById('stat-health').style.color = health < 99 ? 'var(--crypto-red)' : 'var(--crypto-green)';
document.getElementById('mm-health-pct').innerText = health + '% HEALTH';
if (mainChart) {
mainChart.data.datasets[0].data.push(stats.avg_latency);
mainChart.data.datasets[0].data.shift();
mainChart.update('none');
}
const listResp = await fetch('api/uptime.php?action=list');
const assets = await listResp.json();
const inventory = document.getElementById('inventory-list');
const batteryRows = document.getElementById('battery-rows');
const mmCryptoGrid = document.getElementById('mm-crypto-grid');
inventory.innerHTML = '';
batteryRows.innerHTML = '';
assets.forEach(u => {
const isOk = u.last_status === 'ok';
inventory.innerHTML += `<tr><td>${u.url}</td><td><span class="status-badge ${isOk?'bg-ok':'bg-err'}">${u.last_status}</span></td><td>${u.last_latency}ms</td><td><button onclick="deleteAsset(${u.id})" style="color:var(--crypto-red); background:none; padding:0;">DEL</button></td></tr>`;
// Dashboard Battery Rows
const brow = document.createElement('div');
brow.style.marginBottom = '10px';
brow.innerHTML = `<div style="font-size:0.7rem; color:#888; display:flex; justify-content:space-between"><span>${u.url}</span><span>${u.last_latency}ms</span></div>`;
const bcont = document.createElement('div');
bcont.className = 'battery-container';
for(let i=0; i<24; i++) {
const b = document.createElement('div');
b.className = 'battery-bar' + (isOk ? ' ok' : ' err');
bcont.appendChild(b);
}
brow.appendChild(bcont);
batteryRows.appendChild(brow);
// Mobile Crypto Grid
if (!document.getElementById(`mm-row-${u.id}`)) {
const mrow = document.createElement('div');
mrow.id = `mm-row-${u.id}`;
mrow.className = 'mm-row';
mrow.innerHTML = `
<div class="mm-info">
<span class="mm-url-text">${u.url.split('//')[1]}</span>
<span id="mm-lat-${u.id}" class="mm-lat-text" style="color:${isOk?'var(--crypto-green)':'var(--crypto-red)'}">${u.last_latency}ms</span>
</div>
<div class="mm-spark-container">
<canvas id="mm-canvas-${u.id}" class="mm-canvas"></canvas>
</div>
`;
mmCryptoGrid.appendChild(mrow);
sparklines[u.id] = new CryptoSparkline(`mm-canvas-${u.id}`);
} else {
const latEl = document.getElementById(`mm-lat-${u.id}`);
if (latEl) {
latEl.innerText = u.last_latency + 'ms';
latEl.style.color = isOk ? 'var(--crypto-green)' : 'var(--crypto-red)';
}
if (sparklines[u.id]) sparklines[u.id].update(u.last_latency, u.last_status);
}
});
}
async function runPulse() {
if (!isMonitoring) return;
await fetch('api/uptime.php?action=check');
refreshData();
}
async function addAsset() {
const url = document.getElementById('new-url').value;
if (!url) return;
await fetch('api/uptime.php?action=add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
document.getElementById('new-url').value = '';
refreshData();
}
async function deleteAsset(id) {
if (!confirm('Delete?')) return;
await fetch('api/uptime.php?action=delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
refreshData();
}
init();
</script>
</body>
</html>