Autosave: 20260225-010402
This commit is contained in:
parent
2c10ebe286
commit
4fdf45d69d
@ -222,7 +222,7 @@ switch ($action) {
|
|||||||
|
|
||||||
// Send invite email
|
// Send invite email
|
||||||
$subject = "Invitation to join Team on ๐๐จ๐ง๐ข๐ญ๐จ๐ซ ๐๐ฉ๐ญ๐ข๐ฆ๐ ๐๐ฒ ๐๐ฎ๐ฆ๐๐";
|
$subject = "Invitation to join Team on ๐๐จ๐ง๐ข๐ญ๐จ๐ซ ๐๐ฉ๐ญ๐ข๐ฆ๐ ๐๐ฒ ๐๐ฎ๐ฆ๐๐";
|
||||||
$html = "<p>You have been invited to join a team.</p><p><a href='http://".$_SERVER['HTTP_HOST']."'>Accept Invitation</a></p>";
|
$html = "<p>You have been invited to join a team.</p><p><a href='http://".$_SERVER['HTTP_HOST']."/?invite_email=".urlencode($email)."'>Accept Invitation</a></p>";
|
||||||
MailService::sendMail($email, $subject, $html);
|
MailService::sendMail($email, $subject, $html);
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|||||||
BIN
assets/pasted-20260225-004617-36242440.jpg
Normal file
BIN
assets/pasted-20260225-004617-36242440.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
483
index.php
483
index.php
@ -15,7 +15,6 @@ $v = time(); // Cache busting version
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--crypto-green: #00ff88;
|
--crypto-green: #00ff88;
|
||||||
@ -96,7 +95,7 @@ $v = time(); // Cache busting version
|
|||||||
.stat-val { font-family: 'JetBrains Mono', monospace; font-size: 1.8rem; font-weight: 700; display: block; margin-top: 5px; }
|
.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; }
|
.stat-label { color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; }
|
||||||
|
|
||||||
/* Mobile Monitor Specific (CRYPTO CANDLE STYLE) */
|
/* Mobile Monitor Specific */
|
||||||
#mobile-monitor {
|
#mobile-monitor {
|
||||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
background: #000; z-index: 2000; display: none;
|
background: #000; z-index: 2000; display: none;
|
||||||
@ -109,18 +108,30 @@ $v = time(); // Cache busting version
|
|||||||
}
|
}
|
||||||
.mm-grid { display: grid; grid-template-columns: 1fr; gap: 4px; flex: 1; overflow-y: auto; }
|
.mm-grid { display: grid; grid-template-columns: 1fr; gap: 4px; flex: 1; overflow-y: auto; }
|
||||||
.mm-row {
|
.mm-row {
|
||||||
background: #080808; padding: 8px; border-radius: 6px;
|
background: #050505; padding: 12px; border-radius: 8px;
|
||||||
border: 1px solid #1a1a1a; display: flex; flex-direction: column; gap: 4px;
|
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-info { display: flex; justify-content: space-between; align-items: center; font-family: 'JetBrains Mono'; }
|
||||||
.mm-url-text { font-size: 0.55rem; color: #aaa; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 70%; }
|
.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.7rem; font-weight: bold; }
|
.mm-lat-text { font-size: 0.75rem; font-weight: bold; }
|
||||||
.mm-spark-container {
|
.mm-spark-container {
|
||||||
height: 80px; width: 100%; position: relative;
|
height: 240px; width: 100%; position: relative;
|
||||||
background: #050505; border-radius: 4px; overflow: hidden;
|
background: #000; border-radius: 4px; overflow: hidden;
|
||||||
|
border: 1px solid #111;
|
||||||
}
|
}
|
||||||
.mm-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
.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 { 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 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; }
|
.data-table td { padding: 12px; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
|
||||||
@ -134,7 +145,6 @@ $v = time(); // Cache busting version
|
|||||||
.section { display: none; }
|
.section { display: none; }
|
||||||
.section.active { display: block; }
|
.section.active { display: block; }
|
||||||
|
|
||||||
/* Floating Button for Mobile */
|
|
||||||
.fab {
|
.fab {
|
||||||
position: fixed; bottom: 20px; right: 20px;
|
position: fixed; bottom: 20px; right: 20px;
|
||||||
width: 60px; height: 60px; border-radius: 50%;
|
width: 60px; height: 60px; border-radius: 50%;
|
||||||
@ -150,6 +160,8 @@ $v = time(); // Cache busting version
|
|||||||
.content-area { padding: 1rem; }
|
.content-area { padding: 1rem; }
|
||||||
.fab { display: flex; }
|
.fab { display: flex; }
|
||||||
.top-candle-btn { display: flex !important; }
|
.top-candle-btn { display: flex !important; }
|
||||||
|
.dashboard-grid { grid-template-columns: 1fr; }
|
||||||
|
#dashboard-candle-grid, #dashboard-battery-grid { grid-template-columns: 1fr !important; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -167,7 +179,7 @@ $v = time(); // Cache busting version
|
|||||||
<li class="nav-item" onclick="showSection('api')"><span>๐</span> API</li>
|
<li class="nav-item" onclick="showSection('api')"><span>๐</span> API</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div style="padding: 1rem; border-top: 1px solid var(--border);">
|
<div style="padding: 1rem; border-top: 1px solid var(--border);">
|
||||||
<button class="btn-glow" style="width:100%; margin-bottom:10px;" onclick="enterMobileMonitor()">๐ฑ CANDLE MODE</button>
|
<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>
|
<button id="global-toggle" style="width:100%; font-size: 0.8rem;" onclick="toggleMonitoring()">STATION: OFFLINE</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@ -175,7 +187,7 @@ $v = time(); // Cache busting version
|
|||||||
<main>
|
<main>
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<div style="display:flex; align-items:center; gap:10px;">
|
<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()">CANDLE</button>
|
<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);">
|
<div id="ticker" style="font-family: 'JetBrains Mono', monospace; font-size: 0.65rem; color: var(--crypto-green);">
|
||||||
[NET: 100%]
|
[NET: 100%]
|
||||||
</div>
|
</div>
|
||||||
@ -190,7 +202,7 @@ $v = time(); // Cache busting version
|
|||||||
<div id="dashboard" class="section active">
|
<div id="dashboard" class="section active">
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<div class="card" onclick="enterMobileMonitor()" style="cursor:pointer; border: 1px solid var(--crypto-blue);">
|
<div class="card" onclick="enterMobileMonitor()" style="cursor:pointer; border: 1px solid var(--crypto-blue);">
|
||||||
<span class="stat-label">Click for Candle View</span>
|
<span class="stat-label">System Active</span>
|
||||||
<span class="stat-val" id="stat-total">0 Assets</span>
|
<span class="stat-val" id="stat-total">0 Assets</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -203,9 +215,24 @@ $v = time(); // Cache busting version
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<!-- MONITOR (BATTREY) -->
|
||||||
<h3>Latency Graph</h3>
|
<div style="margin-bottom: 40px;">
|
||||||
<canvas id="globalChart" style="height: 180px;"></canvas>
|
<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>
|
</div>
|
||||||
|
|
||||||
@ -225,8 +252,36 @@ $v = time(); // Cache busting version
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="teams" class="section"><div class="card"><h3>Team Management</h3><p>Manage monitoring teams.</p></div></div>
|
<div id="teams" class="section">
|
||||||
<div id="api" class="section"><div class="card"><h3>API Keys</h3><p>External integrations.</p></div></div>
|
<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>
|
||||||
|
<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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -237,7 +292,7 @@ $v = time(); // Cache busting version
|
|||||||
<div class="mm-header">
|
<div class="mm-header">
|
||||||
<div style="display:flex; align-items:center;">
|
<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>
|
<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 CANDLES</span>
|
<span style="font-weight:bold; font-size:0.8rem; color:#fff; font-family:'JetBrains Mono'; letter-spacing:1px;">MONITOR</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:5px;">
|
<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="toggleFullscreen()" style="background:#1a1a1a; color:#888; border:1px solid #333; padding:4px 8px; font-size:0.5rem; border-radius:4px;">FS</button>
|
||||||
@ -252,94 +307,225 @@ $v = time(); // Cache busting version
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
let audioAlert = new Audio('https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3');
|
||||||
|
let previousStatuses = {};
|
||||||
|
|
||||||
|
if ("Notification" in window && Notification.permission !== "granted" && Notification.permission !== "denied") {
|
||||||
|
Notification.requestPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNotifications(assets) {
|
||||||
|
for (const u of assets) {
|
||||||
|
if (previousStatuses[u.id] && previousStatuses[u.id] !== u.last_status) {
|
||||||
|
if (u.last_status === 'error') {
|
||||||
|
if ("Notification" in window && Notification.permission === "granted") {
|
||||||
|
new Notification("๐ด Alert: Monitor Down!", { body: u.url + " is offline!" });
|
||||||
|
}
|
||||||
|
audioAlert.play().catch(e => console.log('Audio play failed', e));
|
||||||
|
} else if (u.last_status === 'ok') {
|
||||||
|
if ("Notification" in window && Notification.permission === "granted") {
|
||||||
|
new Notification("๐ข Alert: Monitor Up!", { body: u.url + " is back online!" });
|
||||||
|
}
|
||||||
|
audioAlert.play().catch(e => console.log('Audio play 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;
|
let isMonitoring = false;
|
||||||
let mainChart = null;
|
|
||||||
const sparklines = {};
|
const sparklines = {};
|
||||||
|
const uptimeHistory = {};
|
||||||
|
let lastDataTime = 0;
|
||||||
|
|
||||||
class CryptoSparkline {
|
class CryptoSparkline {
|
||||||
constructor(canvasId) {
|
constructor(canvasId, initialLogs = []) {
|
||||||
this.canvas = document.getElementById(canvasId);
|
this.canvas = document.getElementById(canvasId);
|
||||||
|
if (!this.canvas) return;
|
||||||
this.ctx = this.canvas.getContext('2d');
|
this.ctx = this.canvas.getContext('2d');
|
||||||
this.data = [];
|
this.data = [];
|
||||||
this.lastVal = Math.random() * 50 + 20;
|
this.maxPoints = 60;
|
||||||
// Pre-fill dummy data
|
|
||||||
for(let i=0; i<45; i++) {
|
if (initialLogs && initialLogs.length > 0) {
|
||||||
this.addCandle(this.lastVal + (Math.random()*10-5), 'ok');
|
[...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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addCandle(val, status) {
|
this.resize();
|
||||||
const open = this.lastVal;
|
window.addEventListener('resize', () => this.resize());
|
||||||
const close = val;
|
setTimeout(() => this.resize(), 100);
|
||||||
const high = Math.max(open, close) + Math.random() * 4;
|
|
||||||
const low = Math.max(0, Math.min(open, close) - Math.random() * 4);
|
this.animate();
|
||||||
this.data.push({ open, close, high, low, ok: status === 'ok' });
|
}
|
||||||
this.lastVal = val;
|
|
||||||
if (this.data.length > 55) this.data.shift();
|
animate() {
|
||||||
|
this.draw();
|
||||||
|
requestAnimationFrame(() => this.animate());
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
|
if (!this.canvas) return;
|
||||||
const rect = this.canvas.parentElement.getBoundingClientRect();
|
const rect = this.canvas.parentElement.getBoundingClientRect();
|
||||||
if (!rect.width) return;
|
if (rect.width <= 0) return;
|
||||||
this.canvas.width = rect.width * window.devicePixelRatio;
|
|
||||||
this.canvas.height = rect.height * window.devicePixelRatio;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
this.canvas.width = rect.width * dpr;
|
||||||
this.draw();
|
this.canvas.height = rect.height * dpr;
|
||||||
|
this.ctx.scale(dpr, dpr);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(val, status) {
|
update(val, status) {
|
||||||
this.addCandle(val, status);
|
// Add subtle jitter to make it look "live" every second
|
||||||
this.draw();
|
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() {
|
draw() {
|
||||||
const w = this.canvas.width / window.devicePixelRatio;
|
if (!this.canvas) return;
|
||||||
const h = this.canvas.height / window.devicePixelRatio;
|
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);
|
this.ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
this.ctx.strokeStyle = '#111';
|
// Draw Perspective Grid
|
||||||
|
this.ctx.strokeStyle = '#0d2218';
|
||||||
this.ctx.lineWidth = 0.5;
|
this.ctx.lineWidth = 0.5;
|
||||||
for(let i=0; i<w; i+=25) {
|
|
||||||
this.ctx.beginPath(); this.ctx.moveTo(i, 0); this.ctx.lineTo(i, h); this.ctx.stroke();
|
for(let i=0; i<10; i++) {
|
||||||
}
|
const y = h - (i * (h/10));
|
||||||
for(let i=0; i<h; i+=15) {
|
this.ctx.beginPath();
|
||||||
this.ctx.beginPath(); this.ctx.moveTo(0, i); this.ctx.lineTo(w, i); this.ctx.stroke();
|
this.ctx.moveTo(0, y);
|
||||||
|
this.ctx.lineTo(w, y);
|
||||||
|
this.ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.data.length < 1) return;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
const allVals = this.data.flatMap(d => [d.high, d.low]);
|
if (this.data.length < 2) return;
|
||||||
const minV = Math.min(...allVals) * 0.8;
|
|
||||||
const maxV = Math.max(...allVals) * 1.2 || 100;
|
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 range = maxV - minV;
|
||||||
|
|
||||||
const step = w / this.data.length;
|
const step = w / (this.data.length - 1);
|
||||||
const candleW = Math.max(2, step * 0.7);
|
|
||||||
|
|
||||||
this.data.forEach((d, i) => {
|
// Draw Area
|
||||||
const x = i * step + (step/2);
|
const gradient = this.ctx.createLinearGradient(0, 0, 0, h);
|
||||||
const color = d.ok ? '#00ff88' : '#ff3366';
|
gradient.addColorStop(0, 'rgba(0, 255, 136, 0.3)');
|
||||||
|
gradient.addColorStop(1, 'rgba(0, 255, 136, 0)');
|
||||||
const yO = h - ((d.open - minV) / range * h);
|
|
||||||
const yC = h - ((d.close - minV) / range * h);
|
|
||||||
const yH = h - ((d.high - minV) / range * h);
|
|
||||||
const yL = h - ((d.low - minV) / range * h);
|
|
||||||
|
|
||||||
this.ctx.beginPath();
|
this.ctx.beginPath();
|
||||||
this.ctx.strokeStyle = color;
|
this.ctx.moveTo(0, h);
|
||||||
this.ctx.lineWidth = 1;
|
this.data.forEach((d, i) => {
|
||||||
this.ctx.moveTo(x, yH);
|
const x = i * step;
|
||||||
this.ctx.lineTo(x, yL);
|
const y = h - ((d.val - minV) / range * h);
|
||||||
this.ctx.stroke();
|
this.ctx.lineTo(x, y);
|
||||||
|
|
||||||
const bY = Math.min(yO, yC);
|
|
||||||
const bH = Math.max(1, Math.abs(yO - yC));
|
|
||||||
|
|
||||||
this.ctx.fillStyle = color;
|
|
||||||
this.ctx.fillRect(x - candleW/2, bY, candleW, bH);
|
|
||||||
});
|
});
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,6 +534,10 @@ $v = time(); // Cache busting version
|
|||||||
const sec = document.getElementById(id);
|
const sec = document.getElementById(id);
|
||||||
if (sec) sec.classList.add('active');
|
if (sec) sec.classList.add('active');
|
||||||
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||||||
|
|
||||||
|
if (id === 'teams') fetchTeams();
|
||||||
|
if (id === 'api') fetchApiKeys();
|
||||||
|
|
||||||
refreshData();
|
refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +554,6 @@ $v = time(); // Cache busting version
|
|||||||
|
|
||||||
function enterMobileMonitor() {
|
function enterMobileMonitor() {
|
||||||
document.getElementById('mobile-monitor').style.display = 'flex';
|
document.getElementById('mobile-monitor').style.display = 'flex';
|
||||||
// Force refresh sparklines
|
|
||||||
Object.values(sparklines).forEach(s => s.resize());
|
Object.values(sparklines).forEach(s => s.resize());
|
||||||
refreshData();
|
refreshData();
|
||||||
}
|
}
|
||||||
@ -378,32 +567,23 @@ $v = time(); // Cache busting version
|
|||||||
const resp = await fetch('api/uptime.php?action=settings&v=<?php echo $v; ?>');
|
const resp = await fetch('api/uptime.php?action=settings&v=<?php echo $v; ?>');
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
isMonitoring = data.monitoring_enabled;
|
isMonitoring = data.monitoring_enabled;
|
||||||
updateStatusUI();
|
|
||||||
initChart();
|
// Auto-login from email invite bypasses login screen
|
||||||
refreshData();
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
setInterval(refreshData, 10000);
|
if (urlParams.has('invite_email')) {
|
||||||
setInterval(runPulse, 15000);
|
localStorage.setItem('userEmail', urlParams.get('invite_email'));
|
||||||
|
window.history.replaceState({}, document.title, "/");
|
||||||
|
alert('Welcome! You are now logged in as ' + localStorage.getItem('userEmail'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function initChart() {
|
updateStatusUI();
|
||||||
const ctx = document.getElementById('globalChart').getContext('2d');
|
refreshData();
|
||||||
mainChart = new Chart(ctx, {
|
|
||||||
type: 'line',
|
// REFRESH UI DATA EVERY 1 SECOND
|
||||||
data: {
|
setInterval(refreshData, 1000);
|
||||||
labels: Array(20).fill(''),
|
|
||||||
datasets: [{
|
// RUN BACKEND CHECK EVERY 2 SECONDS
|
||||||
data: Array(20).fill(0),
|
setInterval(runPulse, 2000);
|
||||||
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() {
|
function updateStatusUI() {
|
||||||
@ -430,7 +610,29 @@ $v = time(); // Cache busting version
|
|||||||
updateStatusUI();
|
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() {
|
async function refreshData() {
|
||||||
|
try {
|
||||||
const statResp = await fetch('api/uptime.php?action=stats&v=<?php echo $v; ?>');
|
const statResp = await fetch('api/uptime.php?action=stats&v=<?php echo $v; ?>');
|
||||||
const stats = await statResp.json();
|
const stats = await statResp.json();
|
||||||
document.getElementById('stat-total').innerText = stats.total + ' Assets';
|
document.getElementById('stat-total').innerText = stats.total + ' Assets';
|
||||||
@ -440,23 +642,69 @@ $v = time(); // Cache busting version
|
|||||||
document.getElementById('stat-health').style.color = health < 99 ? 'var(--crypto-red)' : 'var(--crypto-green)';
|
document.getElementById('stat-health').style.color = health < 99 ? 'var(--crypto-red)' : 'var(--crypto-green)';
|
||||||
document.getElementById('mm-health-pct').innerText = health + '% HEALTH';
|
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&v=<?php echo $v; ?>');
|
const listResp = await fetch('api/uptime.php?action=list&v=<?php echo $v; ?>');
|
||||||
const assets = await listResp.json();
|
const assets = await listResp.json();
|
||||||
|
|
||||||
const inventory = document.getElementById('inventory-list');
|
const inventory = document.getElementById('inventory-list');
|
||||||
const mmCryptoGrid = document.getElementById('mm-crypto-grid');
|
const mmCryptoGrid = document.getElementById('mm-crypto-grid');
|
||||||
|
const dashCandleGrid = document.getElementById('dashboard-candle-grid');
|
||||||
|
const dashBatteryGrid = document.getElementById('dashboard-battery-grid');
|
||||||
|
|
||||||
inventory.innerHTML = '';
|
if (inventory) inventory.innerHTML = '';
|
||||||
|
|
||||||
assets.forEach(u => {
|
for (const u of assets) {
|
||||||
const isOk = u.last_status === 'ok';
|
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>`;
|
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}`)) {
|
if (!document.getElementById(`mm-row-${u.id}`)) {
|
||||||
const mrow = document.createElement('div');
|
const mrow = document.createElement('div');
|
||||||
@ -467,21 +715,31 @@ $v = time(); // Cache busting version
|
|||||||
<span class="mm-url-text">${u.url.replace('https://','').replace('http://','')}</span>
|
<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>
|
<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>
|
||||||
|
<div id="mm-battery-${u.id}" class="uptime-battery"></div>
|
||||||
<div class="mm-spark-container">
|
<div class="mm-spark-container">
|
||||||
<canvas id="mm-canvas-${u.id}" class="mm-canvas"></canvas>
|
<canvas id="mm-canvas-${u.id}" class="mm-canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
mmCryptoGrid.appendChild(mrow);
|
mmCryptoGrid.appendChild(mrow);
|
||||||
sparklines[u.id] = new CryptoSparkline(`mm-canvas-${u.id}`);
|
|
||||||
|
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 {
|
} else {
|
||||||
const latEl = document.getElementById(`mm-lat-${u.id}`);
|
const latEl = document.getElementById(`mm-lat-${u.id}`);
|
||||||
if (latEl) {
|
if (latEl) {
|
||||||
latEl.innerText = u.last_latency + 'ms';
|
latEl.innerText = u.last_latency + 'ms';
|
||||||
latEl.style.color = isOk ? 'var(--crypto-green)' : 'var(--crypto-red)';
|
latEl.style.color = isOk ? 'var(--crypto-green)' : 'var(--crypto-red)';
|
||||||
}
|
}
|
||||||
if (sparklines[u.id]) sparklines[u.id].update(u.last_latency, u.last_status);
|
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() {
|
async function runPulse() {
|
||||||
@ -513,6 +771,11 @@ $v = time(); // Cache busting version
|
|||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
navigator.serviceWorker.register("sw.js").then(reg => console.log("SW registered"));
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loadingโฆ
x
Reference in New Issue
Block a user