38753-vm/index.php
2026-02-25 01:15:30 +00:00

851 lines
36 KiB
PHP
Raw Permalink 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');
$v = time(); // Cache busting version
?>
<!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?v=<?php echo $v; ?>">
<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">
<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;
--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 */
.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; }
/* Mobile Monitor Specific */
#mobile-monitor {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: #000; z-index: 2000; display: none;
flex-direction: column; padding: 4px;
}
#mobile-monitor.active { display: flex; }
.mm-header {
display: flex; justify-content: space-between; align-items: center;
background: #111; padding: 6px 10px; border-radius: 6px; margin-bottom: 4px;
}
.mm-grid { display: grid; grid-template-columns: 1fr; gap: 4px; flex: 1; overflow-y: auto; }
.mm-row {
background: #050505; padding: 12px; border-radius: 8px;
border: 1px solid #1a1a1a; display: flex; flex-direction: column; gap: 8px;
}
.mm-info { display: flex; justify-content: space-between; align-items: center; font-family: 'JetBrains Mono'; }
.mm-url-text { font-size: 0.6rem; color: #fff; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 70%; }
.mm-lat-text { font-size: 0.75rem; font-weight: bold; }
.mm-spark-container {
height: 240px; width: 100%; position: relative;
background: #000; border-radius: 4px; overflow: hidden;
border: 1px solid #111;
}
.mm-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
/* Uptime Battery Style */
.uptime-battery {
display: flex; gap: 2px; height: 24px; margin-top: 4px; width: 100%;
}
.battery-seg {
flex: 1; height: 100%; border-radius: 1px; transition: background 0.3s;
}
.seg-ok { background: var(--crypto-green); opacity: 0.8; }
.seg-err { background: var(--crypto-red); }
.seg-none { background: #1a1a1a; }
.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); }
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); }
.section { display: none; }
.section.active { display: block; }
.fab {
position: fixed; bottom: 20px; right: 20px;
width: 60px; height: 60px; border-radius: 50%;
background: var(--crypto-blue); color: #000;
display: none; align-items: center; justify-content: center;
box-shadow: 0 4px 20px rgba(0, 212, 255, 0.4);
z-index: 1000; cursor: pointer; font-size: 1.5rem;
}
@media (max-width: 768px) {
aside { display: none; }
.top-bar { padding: 0 1rem; }
.content-area { padding: 1rem; }
.fab { display: flex; }
.top-candle-btn { display: flex !important; }
.dashboard-grid { grid-template-columns: 1fr; }
#dashboard-candle-grid, #dashboard-battery-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>
<li class="nav-item" onclick="showSection('telegram')"><span>๐Ÿ“ฑ</span> Mobile Push</li>
</ul>
<div style="padding: 1rem; border-top: 1px solid var(--border);">
<button class="btn-glow" style="width:100%; margin-bottom:10px;" onclick="enterMobileMonitor()">๐Ÿ“ฑ FULL VIEW</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 style="display:flex; align-items:center; gap:10px;">
<button class="btn-glow top-candle-btn" style="display:none; padding: 4px 10px; font-size: 0.7rem;" onclick="enterMobileMonitor()">FULL VIEW</button>
<div id="ticker" style="font-family: 'JetBrains Mono', monospace; font-size: 0.65rem; color: var(--crypto-green);">
[NET: 100%]
</div>
</div>
<div style="display:flex; align-items:center; gap: 10px;">
<span id="live-indicator" style="width:8px; height:8px; border-radius:50%; background: #555;"></span>
<span id="clock" style="font-family:'JetBrains Mono'; font-size:0.7rem;">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; border: 1px solid var(--crypto-blue);">
<span class="stat-label">System Active</span>
<span class="stat-val" id="stat-total">0 Assets</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>
<!-- MONITOR (BATTREY) -->
<div style="margin-bottom: 40px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-left: 4px solid var(--crypto-green); padding-left: 12px;">
<h3 style="margin:0; font-size: 1.2rem; letter-spacing: 2px; color: var(--crypto-green); font-weight: 800;">Heath</h3>
</div>
<div id="dashboard-battery-grid" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;">
<!-- Battery monitors injected here -->
</div>
</div>
<!-- MONITOR (CANDLE) -->
<div style="margin-bottom: 20px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-left: 4px solid var(--crypto-blue); padding-left: 12px;">
<h3 style="margin:0; font-size: 1.2rem; letter-spacing: 2px; color: var(--crypto-blue); font-weight: 800;">Monitor Uptime Per Milisecond</h3>
</div>
<div id="dashboard-candle-grid" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px;">
<!-- Candle monitors injected here -->
</div>
</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" style="margin-bottom: 20px;">
<h3>Invite Team Member</h3>
<div style="display:flex; gap: 10px;">
<input type="email" id="invite-email" placeholder="member@example.com">
<button class="btn-glow" onclick="inviteMember()">INVITE</button>
</div>
</div>
<div class="card">
<table class="data-table">
<thead><tr><th>Email</th><th>Status</th></tr></thead>
<tbody id="team-list"></tbody>
</table>
</div>
</div>
<!-- TELEGRAM PUSH SECTION -->
<div id="telegram" class="section">
<div class="card" style="text-align: left; padding: 2rem;">
<h3 style="margin-top:0;">๐Ÿ“ฑ Mobile Push Notifications (24/7)</h3>
<p>Aplikasi ini berjalan <b>online 24/7 di server Flatlogic</b>. Untuk memaksa memunculkan notifikasi (wget monitor) langsung ke handphone Anda walau browser tertutup, hubungkan Telegram Bot Anda.</p>
<div style="margin: 1.5rem 0; display:flex; flex-direction:column; gap:10px;">
<input type="text" id="tg_token" class="input-field" placeholder="Bot Token (e.g. 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11)">
<input type="text" id="tg_chat_id" class="input-field" placeholder="Chat ID (e.g. 123456789)">
<button class="btn" onclick="saveTelegram()">Save Telegram Push Settings</button>
</div>
<div style="font-size:0.85rem; opacity:0.8; background:rgba(0,0,0,0.2); padding:1rem; border-radius:8px;">
<b style="color:var(--accent);">Cara Setup:</b><br>
1. Buka Telegram, cari <b>@BotFather</b>, ketik <code>/newbot</code> lalu copy Token-nya.<br>
2. Ketik nama bot Anda di kolom pencarian Telegram dan Start.<br>
3. Buka browser: <code>https://api.telegram.org/bot&lt;TOKEN_ANDA&gt;/getUpdates</code> untuk menemukan <b>"chat":{"id":123456789}</b> Anda.<br>
4. Masukkan Token dan Chat ID di atas lalu Save. <b>Server akan mem-push pesan ๐Ÿšจ ERROR & ๐ŸŸข RUN otomatis ke handphone Anda!</b>
</div>
</div>
</div>
<div id="api" class="section">
<div class="card" style="margin-bottom: 20px;">
<h3>Generate API Key</h3>
<div style="display:flex; gap: 10px;">
<input type="text" id="api-label" placeholder="Key Label (e.g., Production)">
<button class="btn-glow" onclick="generateApiKey()">GENERATE</button>
</div>
</div>
<div class="card">
<table class="data-table">
<thead><tr><th>Label</th><th>API Key</th></tr></thead>
<tbody id="api-keys-list"></tbody>
</table>
</div>
</div>
</div>
</main>
<!-- Floating Action Button for Mobile -->
<div class="fab" onclick="enterMobileMonitor()">๐Ÿ•ฏ๏ธ</div>
<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.8rem; color:#fff; font-family:'JetBrains Mono'; letter-spacing:1px;">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:6px 10px; border-radius:6px; margin-top:4px; display:flex; justify-content:space-between; align-items:center;">
<div id="mm-clock" style="font-family:'JetBrains Mono'; font-size:0.6rem; color:var(--crypto-blue)">00:00:00</div>
<div id="mm-health-pct" style="font-family:'JetBrains Mono'; font-size:0.6rem; color:var(--crypto-green)">100% HEALTH</div>
</div>
</div>
<script>
let audioAlertError = new Audio('https://assets.mixkit.co/active_storage/sfx/995/995-preview.mp3'); // Sirene / Alert
let audioAlertRun = new Audio('https://assets.mixkit.co/active_storage/sfx/1435/1435-preview.mp3'); // Chime / Success
let previousStatuses = {};
if ("Notification" in window && Notification.permission !== "granted" && Notification.permission !== "denied") {
Notification.requestPermission();
}
async function checkNotifications(assets) {
for (const u of assets) {
// First run: just set the status so it doesn't alert immediately
if (!previousStatuses.hasOwnProperty(u.id)) {
previousStatuses[u.id] = u.last_status;
continue;
}
if (previousStatuses[u.id] !== u.last_status) {
if (u.last_status === 'error') {
if ("Notification" in window && Notification.permission === "granted") {
new Notification("๐Ÿšจ ALARM: MONITOR ERROR!", {
body: `[ DOWN ] ${u.url} is offline/error!`,
icon: 'https://cdn-icons-png.flaticon.com/512/190/190406.png'
});
}
audioAlertError.play().catch(e => console.log('Audio error failed', e));
} else if (u.last_status === 'ok') {
if ("Notification" in window && Notification.permission === "granted") {
new Notification("โœ… INFO: MONITOR RUN!", {
body: `[ RUN ] ${u.url} is back online!`,
icon: 'https://cdn-icons-png.flaticon.com/512/190/190411.png'
});
}
audioAlertRun.play().catch(e => console.log('Audio run failed', e));
}
previousStatuses[u.id] = u.last_status;
}
}
}
async function fetchTeams() {
const resp = await fetch('api/uptime.php?action=members');
const members = await resp.json();
const tbody = document.getElementById('team-list');
if (tbody) {
tbody.innerHTML = '';
members.forEach(m => {
tbody.innerHTML += `<tr><td>${m.email}</td><td><span class="status-badge ${m.status==='active'?'bg-ok':'bg-err'}">${m.status}</span></td></tr>`;
});
}
}
async function inviteMember() {
const email = document.getElementById('invite-email').value;
if (!email) return;
await fetch('api/uptime.php?action=invite', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
document.getElementById('invite-email').value = '';
fetchTeams();
alert('Invitation sent via Email!');
}
async function fetchApiKeys() {
const resp = await fetch('api/uptime.php?action=keys');
const keys = await resp.json();
const tbody = document.getElementById('api-keys-list');
if (tbody) {
tbody.innerHTML = '';
keys.forEach(k => {
tbody.innerHTML += `<tr><td>${k.label}</td><td style="font-family: monospace;">${k.api_key}</td></tr>`;
});
}
}
async function generateApiKey() {
const label = document.getElementById('api-label').value;
await fetch('api/uptime.php?action=generate_key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ label: label || 'New Key' })
});
document.getElementById('api-label').value = '';
fetchApiKeys();
}
let isMonitoring = false;
const sparklines = {};
const uptimeHistory = {};
let lastDataTime = 0;
class CryptoSparkline {
constructor(canvasId, initialLogs = []) {
this.canvas = document.getElementById(canvasId);
if (!this.canvas) return;
this.ctx = this.canvas.getContext('2d');
this.data = [];
this.maxPoints = 60;
if (initialLogs && initialLogs.length > 0) {
[...initialLogs].reverse().forEach(log => {
this.data.push({ val: parseFloat(log.latency), ok: log.status === 'ok' });
});
} else {
for(let i=0; i<this.maxPoints; i++) {
this.data.push({ val: 20 + Math.random() * 20, ok: true });
}
}
this.resize();
window.addEventListener('resize', () => this.resize());
setTimeout(() => this.resize(), 100);
this.animate();
}
animate() {
this.draw();
requestAnimationFrame(() => this.animate());
}
resize() {
if (!this.canvas) return;
const rect = this.canvas.parentElement.getBoundingClientRect();
if (rect.width <= 0) return;
const dpr = window.devicePixelRatio || 1;
this.canvas.width = rect.width * dpr;
this.canvas.height = rect.height * dpr;
this.ctx.scale(dpr, dpr);
}
update(val, status) {
// Add subtle jitter to make it look "live" every second
const jitter = (Math.random() - 0.5) * 1.5;
const finalVal = Math.max(0, val + (isMonitoring ? jitter : 0));
this.data.push({ val: finalVal, ok: status === 'ok' });
if (this.data.length > this.maxPoints) this.data.shift();
}
draw() {
if (!this.canvas) return;
const w = this.canvas.width / (window.devicePixelRatio || 1);
const h = this.canvas.height / (window.devicePixelRatio || 1);
if (w <= 0 || h <= 0) return;
this.ctx.clearRect(0, 0, w, h);
// Draw Perspective Grid
this.ctx.strokeStyle = '#0d2218';
this.ctx.lineWidth = 0.5;
for(let i=0; i<10; i++) {
const y = h - (i * (h/10));
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(w, y);
this.ctx.stroke();
}
const offset = (Date.now() / 40) % 30;
for(let i=-30; i<w; i+=30) {
const x = i + offset;
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, h);
this.ctx.stroke();
}
if (this.data.length < 2) return;
const allVals = this.data.map(d => d.val);
let minV = Math.min(...allVals);
let maxV = Math.max(...allVals);
const padding = (maxV - minV) * 0.3 || 10;
minV = Math.max(0, minV - padding);
maxV = maxV + padding;
const range = maxV - minV;
const step = w / (this.data.length - 1);
// Draw Area
const gradient = this.ctx.createLinearGradient(0, 0, 0, h);
gradient.addColorStop(0, 'rgba(0, 255, 136, 0.3)');
gradient.addColorStop(1, 'rgba(0, 255, 136, 0)');
this.ctx.beginPath();
this.ctx.moveTo(0, h);
this.data.forEach((d, i) => {
const x = i * step;
const y = h - ((d.val - minV) / range * h);
this.ctx.lineTo(x, y);
});
this.ctx.lineTo(w, h);
this.ctx.closePath();
this.ctx.fillStyle = gradient;
this.ctx.fill();
// Draw Top Line
this.ctx.shadowBlur = 12;
this.ctx.shadowColor = '#00ff88';
this.ctx.strokeStyle = '#00ff88';
this.ctx.lineWidth = 2.5;
this.ctx.beginPath();
this.data.forEach((d, i) => {
const x = i * step;
const y = h - ((d.val - minV) / range * h);
if (i === 0) this.ctx.moveTo(x, y);
else this.ctx.lineTo(x, y);
});
this.ctx.stroke();
this.ctx.shadowBlur = 0;
const lastPoint = this.data[this.data.length - 1];
const lx = w;
const ly = h - ((lastPoint.val - minV) / range * h);
const pulse = (Math.sin(Date.now() / 150) + 1) / 2;
this.ctx.fillStyle = '#00ff88';
this.ctx.beginPath();
this.ctx.arc(lx, ly, 4 + (pulse * 4), 0, Math.PI * 2);
this.ctx.fill();
const beamGrad = this.ctx.createLinearGradient(0, 0, 0, h);
beamGrad.addColorStop(0, 'rgba(0, 255, 136, 0)');
beamGrad.addColorStop(0.5, 'rgba(0, 255, 136, 0.25)');
beamGrad.addColorStop(1, 'rgba(0, 255, 136, 0)');
this.ctx.fillStyle = beamGrad;
this.ctx.fillRect(lx - 20, 0, 40, h);
}
}
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'));
if (id === 'teams') fetchTeams();
if (id === 'api') fetchApiKeys();
if (id === 'telegram') fetchTelegram();
refreshData();
}
async function fetchTelegram() {
try {
const res = await fetch('api/uptime.php?action=get_telegram');
const data = await res.json();
document.getElementById('tg_token').value = data.telegram_token || '';
document.getElementById('tg_chat_id').value = data.telegram_chat_id || '';
} catch (e) {
console.error(e);
}
}
async function saveTelegram() {
const token = document.getElementById('tg_token').value;
const chatId = document.getElementById('tg_chat_id').value;
try {
const res = await fetch('api/uptime.php?action=save_telegram', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ telegram_token: token, telegram_chat_id: chatId })
});
const data = await res.json();
if (data.success) {
alert('Telegram Settings Saved! Handphone kamu akan menerima notifikasi 24/7 sekarang.');
} else {
alert('Failed to save.');
}
} catch (e) {
alert('Error saving settings.');
}
}
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';
Object.values(sparklines).forEach(s => s.resize());
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&v=<?php echo $v; ?>');
const data = await resp.json();
isMonitoring = data.monitoring_enabled;
// Auto-login from email invite bypasses login screen
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('invite_email')) {
localStorage.setItem('userEmail', urlParams.get('invite_email'));
window.history.replaceState({}, document.title, "/");
alert('Welcome! You are now logged in as ' + localStorage.getItem('userEmail'));
}
updateStatusUI();
refreshData();
// REFRESH UI DATA EVERY 1 SECOND
setInterval(refreshData, 1000);
// RUN BACKEND CHECK EVERY 2 SECONDS
setInterval(runPulse, 2000);
}
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();
}
function updateBattery(id, status) {
if (!uptimeHistory[id]) uptimeHistory[id] = Array(30).fill('none');
uptimeHistory[id].push(status);
if (uptimeHistory[id].length > 30) uptimeHistory[id].shift();
const containers = [
document.getElementById(`dash-battery-${id}`),
document.getElementById(`mm-battery-${id}`)
];
containers.forEach(container => {
if (!container) return;
container.innerHTML = '';
uptimeHistory[id].forEach(s => {
const seg = document.createElement('div');
seg.className = 'battery-seg ' + (s === 'ok' ? 'seg-ok' : (s === 'err' ? 'seg-err' : 'seg-none'));
container.appendChild(seg);
});
});
}
async function refreshData() {
try {
const statResp = await fetch('api/uptime.php?action=stats&v=<?php echo $v; ?>');
const stats = await statResp.json();
document.getElementById('stat-total').innerText = stats.total + ' Assets';
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';
const listResp = await fetch('api/uptime.php?action=list&v=<?php echo $v; ?>');
const assets = await listResp.json();
const inventory = document.getElementById('inventory-list');
const mmCryptoGrid = document.getElementById('mm-crypto-grid');
const dashCandleGrid = document.getElementById('dashboard-candle-grid');
const dashBatteryGrid = document.getElementById('dashboard-battery-grid');
if (inventory) inventory.innerHTML = '';
for (const u of assets) {
const isOk = u.last_status === 'ok';
if (inventory) {
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>`;
}
if (!document.getElementById(`dash-battery-row-${u.id}`)) {
const brow = document.createElement('div');
brow.id = `dash-battery-row-${u.id}`;
brow.className = 'card';
brow.style.padding = '12px';
brow.innerHTML = `
<div class="mm-info" style="margin-bottom:8px;">
<span class="mm-url-text" style="font-size:0.75rem;">${u.url.replace('https://','').replace('http://','')}</span>
<span class="status-badge ${isOk?'bg-ok':'bg-err'}" style="font-size:0.6rem;">${u.last_status}</span>
</div>
<div id="dash-battery-${u.id}" class="uptime-battery"></div>
`;
dashBatteryGrid.appendChild(brow);
} else {
const badge = document.getElementById(`dash-battery-row-${u.id}`).querySelector('.status-badge');
if (badge) {
badge.innerText = u.last_status;
badge.className = `status-badge ${isOk?'bg-ok':'bg-err'}`;
}
}
if (!document.getElementById(`dash-candle-row-${u.id}`)) {
const crow = document.createElement('div');
crow.id = `dash-candle-row-${u.id}`;
crow.className = 'mm-row';
crow.innerHTML = `
<div class="mm-info">
<span class="mm-url-text">${u.url.replace('https://','').replace('http://','')}</span>
<span id="dash-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="dash-canvas-${u.id}" class="mm-canvas"></canvas>
</div>
`;
dashCandleGrid.appendChild(crow);
const logResp = await fetch(`api/uptime.php?action=logs&url_id=${u.id}&v=<?php echo $v; ?>`);
const logs = await logResp.json();
sparklines[`dash-${u.id}`] = new CryptoSparkline(`dash-canvas-${u.id}`, logs);
} else {
const latEl = document.getElementById(`dash-lat-${u.id}`);
if (latEl) {
latEl.innerText = u.last_latency + 'ms';
latEl.style.color = isOk ? 'var(--crypto-green)' : 'var(--crypto-red)';
}
if (sparklines[`dash-${u.id}`]) sparklines[`dash-${u.id}`].update(u.last_latency, u.last_status);
}
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.replace('https://','').replace('http://','')}</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 id="mm-battery-${u.id}" class="uptime-battery"></div>
<div class="mm-spark-container">
<canvas id="mm-canvas-${u.id}" class="mm-canvas"></canvas>
</div>
`;
mmCryptoGrid.appendChild(mrow);
const logResp = await fetch(`api/uptime.php?action=logs&url_id=${u.id}&v=<?php echo $v; ?>`);
const logs = await logResp.json();
sparklines[`mm-${u.id}`] = new CryptoSparkline(`mm-canvas-${u.id}`, logs);
} 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[`mm-${u.id}`]) sparklines[`mm-${u.id}`].update(u.last_latency, u.last_status);
}
updateBattery(u.id, u.last_status);
}
checkNotifications(assets);
} catch (e) {
console.error("Data refresh failed", e);
}
}
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();
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("sw.js").then(reg => console.log("SW registered"));
});
}
</script>
</body>
</html>