Flatlogic Bot cde29a9feb Uptime
2026-02-25 06:46:57 +00:00

288 lines
11 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const monitorList = document.getElementById('monitor-list');
const addForm = document.getElementById('add-monitor-form');
const serviceStatus = document.getElementById('service-status');
let isFetching = false;
const fetchMonitors = async (action = 'list') => {
if (isFetching) return;
isFetching = true;
try {
const response = await fetch(`api/monitor.php?action=${action}`);
const result = await response.json();
if (result.success) {
renderMonitors(result.data);
updateSummary(result.data);
updateServiceStatus(result.worker_active);
}
} catch (error) {
console.error('Error fetching monitors:', error);
} finally {
isFetching = false;
}
};
const updateServiceStatus = (active) => {
if (!serviceStatus) return;
const text = serviceStatus.querySelector('.status-text');
if (active) {
serviceStatus.classList.remove('border-danger');
serviceStatus.classList.add('border-cyan');
text.innerText = 'Online';
text.className = 'status-text text-success';
} else {
serviceStatus.classList.remove('border-cyan');
serviceStatus.classList.add('border-danger');
text.innerText = 'Offline (Restarting...)';
text.className = 'status-text text-danger';
}
};
const updateSummary = (monitors) => {
const total = monitors.length;
const online = monitors.filter(m => m.last_status >= 200 && m.last_status < 400).length;
const offline = total - online;
const onlineEl = document.getElementById('summary-online');
const offlineEl = document.getElementById('summary-offline');
const totalEl = document.getElementById('summary-total');
if (onlineEl) onlineEl.innerText = online;
if (offlineEl) offlineEl.innerText = offline;
if (totalEl) totalEl.innerText = total;
};
const renderMonitors = (monitors) => {
if (monitors.length === 0) {
monitorList.innerHTML = '<div class="col-12 text-center opacity-50 p-5">No URLs added yet. Start by adding one!</div>';
return;
}
const currentCount = monitorList.querySelectorAll('.monitor-card').length;
if (currentCount !== monitors.length) {
monitorList.innerHTML = monitors.map(m => createMonitorHTML(m)).join('');
attachListeners();
} else {
monitors.forEach(m => updateMonitorDOM(m));
}
monitors.forEach(m => {
const canvas = document.getElementById(`sparkline-${m.id}`);
if (canvas) drawSparkline(canvas, m.history || []);
});
};
const createMonitorHTML = (m) => {
const history = m.history || [];
const segments = Array(30).fill('<div class="health-segment segment-none"></div>');
history.forEach((h, idx) => {
const isUp = h.status_code >= 200 && h.status_code < 400;
const segIdx = segments.length - history.length + idx;
segments[segIdx] = `<div class="health-segment ${isUp ? 'segment-up' : 'segment-down'}"></div>`;
});
return `
<div class="col-md-6 col-lg-4 mb-4">
<div class="card monitor-card h-100" id="monitor-${m.id}">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0 text-truncate" style="max-width: 70%; font-weight: 700;">${m.name}</h5>
<div class="d-flex align-items-center">
<span class="status-indicator ${getStatusClass(m.last_status)}"></span>
<small class="status-text opacity-75">${m.last_status || '---'}</small>
</div>
</div>
<p class="card-text small text-muted text-truncate mb-3">${m.url}</p>
<div class="stats-label mb-1">Health Status (Real-time)</div>
<div class="health-bars mb-3">
${segments.join('')}
</div>
<div class="sparkline-container mb-3">
<canvas class="sparkline-canvas" id="sparkline-${m.id}" width="400" height="100" style="width: 100%; height: 60px;"></canvas>
</div>
<div class="row g-2 mb-3 text-center">
<div class="col-6">
<div class="stats-label">Latency</div>
<div class="stats-value latency-value">${m.last_latency || 0}ms</div>
</div>
<div class="col-6">
<div class="stats-label">Live Feed</div>
<div class="stats-value"><span class="badge bg-primary pulse-mini">LIVE</span></div>
</div>
</div>
<div class="d-flex justify-content-between gap-2 mt-auto">
<button class="neon-btn flex-grow-1 ping-btn" data-id="${m.id}">
<i class="bi bi-play-fill"></i> Test
</button>
<button class="btn btn-outline-danger btn-sm border-0 opacity-50 delete-btn" data-id="${m.id}">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
`;
};
const updateMonitorDOM = (m) => {
const card = document.getElementById(`monitor-${m.id}`);
if (!card) return;
const indicator = card.querySelector('.status-indicator');
indicator.className = `status-indicator ${getStatusClass(m.last_status)}`;
const statusText = card.querySelector('.status-text');
statusText.innerText = m.last_status || '---';
const latencyVal = card.querySelector('.latency-value');
latencyVal.innerText = (m.last_latency || 0) + 'ms';
const history = m.history || [];
const barContainer = card.querySelector('.health-bars');
const segments = Array(30).fill('<div class="health-segment segment-none"></div>');
history.forEach((h, idx) => {
const isUp = h.status_code >= 200 && h.status_code < 400;
const segIdx = segments.length - history.length + idx;
segments[segIdx] = `<div class="health-segment ${isUp ? 'segment-up' : 'segment-down'}"></div>`;
});
barContainer.innerHTML = segments.join('');
};
const getStatusClass = (code) => {
if (!code || code === 0) return 'status-unknown';
if (code >= 200 && code < 400) return 'status-online';
return 'status-offline';
};
const drawSparkline = (canvas, data) => {
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
if (data.length < 2) return;
const points = data.length;
const xStep = w / (points - 1);
const latencies = data.map(d => parseInt(d.latency) || 0);
const maxLat = Math.max(...latencies, 500);
const getColor = (status) => (status >= 200 && status < 400) ? '#00ff88' : '#ff4d4d';
ctx.beginPath();
ctx.moveTo(0, h);
data.forEach((d, idx) => {
const x = idx * xStep;
const val = parseInt(d.latency) || 0;
const y = h - (val / maxLat) * (h * 0.7) - 15;
ctx.lineTo(x, y);
});
ctx.lineTo(w, h);
const fillGrad = ctx.createLinearGradient(0, 0, 0, h);
fillGrad.addColorStop(0, 'rgba(0, 243, 255, 0.15)');
fillGrad.addColorStop(1, 'rgba(0, 243, 255, 0)');
ctx.fillStyle = fillGrad;
ctx.fill();
ctx.lineWidth = 3;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
for (let i = 0; i < data.length - 1; i++) {
const d1 = data[i];
const d2 = data[i + 1];
const x1 = i * xStep;
const y1 = h - (parseInt(d1.latency) || 0) / maxLat * (h * 0.7) - 15;
const x2 = (i + 1) * xStep;
const y2 = h - (parseInt(d2.latency) || 0) / maxLat * (h * 0.7) - 15;
const color = getColor(d2.status_code);
ctx.beginPath();
ctx.strokeStyle = color;
ctx.shadowBlur = 8;
ctx.shadowColor = color;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
};
const attachListeners = () => {
document.querySelectorAll('.ping-btn').forEach(btn => {
const newBtn = btn.cloneNode(true);
btn.parentNode.replaceChild(newBtn, btn);
newBtn.addEventListener('click', () => pingMonitor(newBtn.dataset.id, newBtn));
});
document.querySelectorAll('.delete-btn').forEach(btn => {
const newBtn = btn.cloneNode(true);
btn.parentNode.replaceChild(newBtn, btn);
newBtn.addEventListener('click', () => deleteMonitor(newBtn.dataset.id));
});
};
const pingMonitor = async (id, btn) => {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
try {
// Using list to refresh since worker is active
await fetchMonitors('list');
} finally {
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-play-fill"></i> Test';
}, 500);
}
};
const deleteMonitor = async (id) => {
if (!confirm('Are you sure you want to delete this monitor?')) return;
try {
const response = await fetch(`api/monitor.php?action=delete&id=${id}`);
const result = await response.json();
if (result.success) {
fetchMonitors('list');
}
} catch (error) {
console.error('Delete error:', error);
}
};
addForm.addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = addForm.querySelector('button[type="submit"]');
submitBtn.disabled = true;
const formData = {
name: document.getElementById('mon-name').value,
url: document.getElementById('mon-url').value,
interval: document.getElementById('mon-interval').value
};
try {
const response = await fetch('api/monitor.php?action=add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
addForm.reset();
bootstrap.Modal.getInstance(document.getElementById('addMonitorModal')).hide();
fetchMonitors('list');
} else {
alert('Error adding monitor: ' + result.error);
}
} catch (error) {
console.error('Add error:', error);
} finally {
submitBtn.disabled = false;
}
});
fetchMonitors('list');
setInterval(() => fetchMonitors('ping_all'), 1000);
});