Files
EonaCat.LogStack/EonaCat.LogStack.Status/wwwroot/js/site.js
2026-04-06 08:15:54 +02:00

318 lines
12 KiB
JavaScript
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.
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
'use strict';
function updateClock() {
const now = new Date();
const pad = n => String(n).padStart(2, '0');
const el = document.getElementById('clock');
if (el) el.textContent = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
}
updateClock();
setInterval(updateClock, 1000);
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const main = document.getElementById('main-content');
const btn = sidebar && sidebar.querySelector('.sidebar-toggle');
const collapsed = sidebar.classList.toggle('collapsed');
// Sync main content margin
if (main) main.style.marginLeft = collapsed ? '56px' : '220px';
// Flip the arrow glyph
if (btn) btn.textContent = collapsed ? '⟩' : '⟨';
// Persist preference
try { localStorage.setItem('sidebarCollapsed', collapsed ? '1' : '0'); } catch {}
}
// Restore sidebar state on load
(function () {
try {
const pref = localStorage.getItem('sidebarCollapsed');
if (pref === '1') {
const sidebar = document.getElementById('sidebar');
const main = document.getElementById('main-content');
const btn = sidebar && sidebar.querySelector('.sidebar-toggle');
if (sidebar) { sidebar.classList.add('collapsed'); }
if (main) main.style.marginLeft = '56px';
if (btn) btn.textContent = '⟩';
}
} catch {}
})();
function openModal(id) {
const el = document.getElementById(id);
if (el) el.classList.add('open');
}
function closeModal(id) {
const el = document.getElementById(id);
if (el) el.classList.remove('open');
}
// Close modal on overlay click
document.addEventListener('click', function (e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.classList.remove('open');
}
});
// Close modal on Escape
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.open').forEach(m => m.classList.remove('open'));
}
});
function showToast(msg, type = 'info', durationMs = 4000) {
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const icons = { success: '✓', error: '✕', warn: '⚠', info: '' };
toast.innerHTML = `<span>${icons[type] ?? ''}</span><span>${msg}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s';
setTimeout(() => toast.remove(), 300);
}, durationMs);
}
const AUTO_REFRESH_SECS = 30;
(function () {
const pageEl = document.querySelector('[data-autorefresh]');
if (!pageEl) return;
const ringEl = document.getElementById('refresh-ring');
const fillEl = document.getElementById('refresh-ring-fill');
const labelEl = document.getElementById('refresh-ring-label');
const circumference = 50.27; // 2π × 8
if (ringEl) ringEl.style.display = 'inline-flex';
let remaining = AUTO_REFRESH_SECS;
function tick() {
remaining--;
if (labelEl) labelEl.textContent = remaining;
if (fillEl) {
const offset = circumference * (1 - remaining / AUTO_REFRESH_SECS);
fillEl.style.strokeDashoffset = offset;
}
if (remaining <= 0) location.reload();
}
if (labelEl) labelEl.textContent = remaining;
if (fillEl) fillEl.style.strokeDashoffset = 0;
setInterval(tick, 1000);
})();
fetch('/api/status/summary')
.then(r => r.json())
.then(d => {
const dot = document.getElementById('overall-dot');
if (!dot) return;
if (d.downCount > 0) dot.style.background = 'var(--down)';
else if (d.warnCount > 0) dot.style.background = 'var(--warn)';
else if (d.upCount > 0) dot.style.background = 'var(--up)';
else dot.style.background = 'var(--unknown)';
})
.catch(() => {});
(function () {
const input = document.getElementById('log-search');
if (!input) return;
let timer;
input.addEventListener('input', function () {
clearTimeout(timer);
timer = setTimeout(() => {
const form = input.closest('form');
if (form) form.submit();
}, 400);
});
})();
function scrollLogsToBottom() {
const el = document.getElementById('log-stream');
if (el) el.scrollTop = el.scrollHeight;
}
document.addEventListener('DOMContentLoaded', scrollLogsToBottom);
(function () {
const params = new URLSearchParams(window.location.search);
const term = params.get('Search') || params.get('search');
if (!term || term.length < 2) return;
const stream = document.getElementById('log-stream');
if (!stream) return;
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
stream.querySelectorAll('.log-message').forEach(el => {
el.innerHTML = el.textContent.replace(regex, '<mark>$1</mark>');
});
})();
(function () {
const stream = document.getElementById('log-stream');
const liveCountEl = document.getElementById('live-count');
if (!stream || !liveCountEl) return;
let lastId = parseInt(stream.dataset.lastId || '0', 10);
async function pollLogs() {
try {
const res = await fetch(`/api/logs?page=1&pageSize=20`);
if (!res.ok) return;
const data = await res.json();
const newEntries = (data.entries || []).filter(e => e.id > lastId);
if (newEntries.length === 0) return;
newEntries.reverse().forEach(e => {
const div = document.createElement('div');
div.className = 'log-entry';
div.innerHTML = `
<span class="log-ts">${e.timestamp.replace('T', ' ').substring(0, 19)}</span>
<span class="log-level ${e.level}">${e.level.toUpperCase()}</span>
<span class="log-source">${e.source || ''}</span>
<span class="log-message">${escHtml(e.message)}</span>`;
stream.prepend(div);
lastId = Math.max(lastId, e.id);
});
if (liveCountEl) liveCountEl.textContent = `+${newEntries.length} new`;
setTimeout(() => { if (liveCountEl) liveCountEl.textContent = ''; }, 2500);
} catch {}
}
setInterval(pollLogs, 5000);
})();
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
document.addEventListener('keydown', function (e) {
// Don't fire inside inputs / textareas
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return;
switch (e.key) {
case 'b': toggleSidebar(); break; // B = toggle sidebar
case 'g':
if (e.shiftKey) { window.location.href = '/'; } // G = dashboard
break;
case 'l':
if (e.shiftKey) { window.location.href = '/logs'; } // L = logs
break;
case 'm':
if (e.shiftKey) { window.location.href = '/monitors';} // M = monitors
break;
case 'r': location.reload(); break; // R = refresh
case '/': { // / = focus search
const s = document.getElementById('log-search');
if (s) { e.preventDefault(); s.focus(); }
break;
}
}
});
function copyText(text, label) {
navigator.clipboard.writeText(text).then(() => {
showToast(`${label || 'Copied'} to clipboard`, 'success', 2000);
}).catch(() => showToast('Copy failed', 'error'));
}
function confirmDelete(name) {
return confirm(`Delete "${name}"? This cannot be undone.`);
}
function drawSparkline(canvas, values, color) {
if (!canvas || !values || values.length === 0) return;
const ctx = canvas.getContext('2d');
const w = canvas.width, h = canvas.height;
const max = Math.max(...values, 1);
const min = 0;
const step = w / (values.length - 1 || 1);
ctx.clearRect(0, 0, w, h);
ctx.strokeStyle = color || 'var(--accent)';
ctx.lineWidth = 1.5;
ctx.lineJoin = 'round';
ctx.beginPath();
values.forEach((v, i) => {
const x = i * step;
const y = h - ((v - min) / (max - min)) * h * 0.9 - h * 0.05;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
function formatMs(ms) {
if (ms < 1000) return `${Math.round(ms)}ms`;
return `${(ms / 1000).toFixed(1)}s`;
}
function buildUptimeRing(pct, size = 36) {
const r = (size / 2) - 3;
const circ = 2 * Math.PI * r;
const fill = circ * (pct / 100);
const color = pct >= 99 ? 'var(--up)' : pct >= 95 ? 'var(--warn)' : 'var(--down)';
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="transform:rotate(-90deg)">
<circle cx="${size/2}" cy="${size/2}" r="${r}" fill="none" stroke="var(--border)" stroke-width="3"/>
<circle cx="${size/2}" cy="${size/2}" r="${r}" fill="none" stroke="${color}" stroke-width="3"
stroke-dasharray="${fill} ${circ}" stroke-linecap="round"/>
</svg>
<span style="position:absolute;font-size:9px;font-family:var(--font-mono);color:${color}">${Math.round(pct)}%</span>`;
}
async function loadLogStatsChart(canvasId) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
try {
const res = await fetch('/api/logs/stats?hours=24');
if (!res.ok) return;
const buckets = await res.json();
if (!buckets.length) return;
const labels = buckets.map(b => {
const d = new Date(b.bucketStart);
return `${String(d.getHours()).padStart(2,'0')}:00`;
});
const totals = buckets.map(b => b.total);
const errors = buckets.map(b => b.errors);
const warnings = buckets.map(b => b.warnings);
// Use Chart.js if available, otherwise draw with canvas API
if (window.Chart) {
new window.Chart(canvas, {
type: 'bar',
data: {
labels,
datasets: [
{ label: 'Total', data: totals, backgroundColor: 'rgba(91,156,246,0.25)', borderColor: 'rgba(91,156,246,0.7)', borderWidth: 1 },
{ label: 'Errors', data: errors, backgroundColor: 'rgba(255,75,110,0.25)', borderColor: 'rgba(255,75,110,0.7)', borderWidth: 1 },
{ label: 'Warnings', data: warnings, backgroundColor: 'rgba(255,181,71,0.2)', borderColor: 'rgba(255,181,71,0.6)', borderWidth: 1 },
]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { labels: { color: '#8b8fa8', font: { family: 'Space Mono', size: 10 } } } },
scales: {
x: { ticks: { color: '#4e5268', font: { family: 'Space Mono', size: 9 } }, grid: { color: 'rgba(255,255,255,0.04)' } },
y: { ticks: { color: '#4e5268', font: { family: 'Space Mono', size: 9 } }, grid: { color: 'rgba(255,255,255,0.04)' } }
}
}
});
} else {
// Fallback: simple bar sparkline
drawSparkline(canvas, totals, 'rgba(91,156,246,0.7)');
}
} catch {}
}