288 lines
11 KiB
JavaScript
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);
|
|
}); |