Files
ntfy-bridge/dashboard.html
T
bruno 5d0db9478c
CI / test (push) Successful in 2m33s
fix: dashboard affiche la version dynamique ({{VERSION}})
dashboard.html utilise {{VERSION}}, remplacé par server.v au chargement
via const version de main.v. Plus besoin de maj manuelle du HTML.
2026-08-03 17:24:14 -04:00

173 lines
7.4 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ntfy-bridge Dashboard</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh}
header{background:#1e293b;border-bottom:1px solid #334155;padding:16px 24px;display:flex;justify-content:space-between;align-items:center}
header h1{font-size:1.25rem;font-weight:600;color:#f1f5f9}
header .badge{background:#16a34a;color:#fff;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600}
.container{max-width:1200px;margin:0 auto;padding:24px}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:20px;margin-bottom:24px}
.card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:20px}
.card h2{font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8;margin-bottom:16px;display:flex;align-items:center;gap:8px}
.card h2 .icon{font-size:1.1rem}
.stat-value{font-size:2rem;font-weight:700;color:#f1f5f9;line-height:1.2}
.stat-label{font-size:.75rem;color:#64748b;margin-top:4px}
.stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:8px}
.status-up{background:#16a34a}
.status-down{background:#dc2626}
.status-item{display:flex;align-items:center;padding:8px 0;border-bottom:1px solid #334155;font-size:.875rem}
.status-item:last-child{border-bottom:none}
.config-row{display:flex;justify-content:space-between;padding:6px 0;font-size:.8125rem;border-bottom:1px solid #1e293b}
.config-row:last-child{border-bottom:none}
.config-key{color:#94a3b8}
.config-val{color:#e2e8f0;font-weight:500}
.history-item{padding:10px 0;border-bottom:1px solid #334155;font-size:.8125rem}
.history-item:last-child{border-bottom:none}
.history-meta{display:flex;gap:12px;color:#64748b;margin-bottom:4px;flex-wrap:wrap}
.history-msg{color:#e2e8f0;word-break:break-all}
.priority-tag{display:inline-block;padding:1px 6px;border-radius:4px;font-size:.6875rem;font-weight:700}
.p5{background:#7f1d1d;color:#fca5a5}
.p4{background:#78350f;color:#fcd34d}
.p3{background:#1e3a5f;color:#93c5fd}
.p2{background:#1e293b;color:#94a3b8}
.p1{background:#0f172a;color:#64748b}
.no-data{color:#64748b;font-style:italic;text-align:center;padding:24px 0}
.tabs{display:flex;gap:4px;margin-bottom:16px;background:#0f172a;border-radius:6px;padding:4px}
.tab{padding:6px 16px;border-radius:4px;font-size:.8125rem;cursor:pointer;color:#94a3b8;border:none;background:none}
.tab.active{background:#334155;color:#f1f5f9}
.tab-content{display:none}
.tab-content.active{display:block}
footer{text-align:center;padding:24px;color:#475569;font-size:.75rem}
</style>
</head>
<body>
<header>
<h1>🔔 ntfy-bridge</h1>
<span class="badge" id="uptime-badge">LIVE</span>
</header>
<div class="container">
<div class="grid">
<div class="card">
<h2><span class="icon">📊</span> Notifications</h2>
<div class="stat-grid">
<div><div class="stat-value" id="total-notif">—</div><div class="stat-label">Total</div></div>
<div><div class="stat-value" id="total-errors">—</div><div class="stat-label">Errors</div></div>
</div>
</div>
<div class="card">
<h2><span class="icon">⏱️</span> Uptime</h2>
<div class="stat-value" id="uptime">—</div>
<div class="stat-label" id="uptime-human">Server running</div>
</div>
<div class="card">
<h2><span class="icon">🔗</span> Ntfy Server</h2>
<div class="stat-value" style="font-size:1rem" id="ntfy-url">—</div>
<div class="stat-label">Target</div>
</div>
</div>
<div class="grid">
<div class="card" style="grid-column:span 2">
<h2><span class="icon">📋</span> Details</h2>
<div class="tabs">
<button class="tab active" onclick="switchTab('status')">Service Status</button>
<button class="tab" onclick="switchTab('config')">Configuration</button>
<button class="tab" onclick="switchTab('history')">Recent History</button>
</div>
<div class="tab-content active" id="tab-status">
<div id="status-list" class="no-data">Loading...</div>
</div>
<div class="tab-content" id="tab-config">
<div id="config-list" class="no-data">Loading...</div>
</div>
<div class="tab-content" id="tab-history">
<div id="history-list" class="no-data">Loading...</div>
</div>
</div>
</div>
</div>
<footer>ntfy-bridge {{VERSION}} · Homelab Notification Hub</footer>
<script>
function switchTab(name) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
var sel = '.tab[onclick*="switchTab(\'' + name + '\')"]';
document.querySelector(sel).classList.add('active');
document.getElementById('tab-' + name).classList.add('active');
}
async function fetchJSON(url) {
var r = await fetch(url);
if (!r.ok) throw new Error(r.status);
return r.json();
}
function fmtUptime(s) {
var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
if (h > 24) { var d = Math.floor(h / 24); return d + 'd ' + (h % 24) + 'h'; }
return h + 'h ' + m + 'm';
}
async function load() {
try {
var health = await fetchJSON('/health');
document.getElementById('total-notif').textContent = health.total_notifications;
document.getElementById('uptime').textContent = fmtUptime(health.uptime_seconds);
document.getElementById('uptime-human').textContent = 'since ' + new Date(Date.now() - health.uptime_seconds * 1000).toLocaleString();
var stats = await fetchJSON('/stats');
document.getElementById('total-errors').textContent = stats.errors;
var config = await fetchJSON('/api/config');
document.getElementById('ntfy-url').textContent = config.server_url;
var statusData = await fetchJSON('/api/status');
var sl = document.getElementById('status-list');
if (!statusData || statusData.length === 0) {
sl.innerHTML = '<div class=no-data>No HTTP poll checks configured</div>';
} else {
sl.innerHTML = statusData.map(function(s) {
var cls = s.status === 'up' ? 'status-up' : 'status-down';
return '<div class=status-item><span class="status-dot ' + cls + '"></span>' + s.name + ' &mdash; <strong>' + s.status.toUpperCase() + '</strong></div>';
}).join('');
}
var cl = document.getElementById('config-list');
cl.innerHTML = Object.entries(config).map(function(e) {
var k = e[0], v = e[1];
return '<div class=config-row><span class=config-key>' + k.replace(/_/g, ' ') + '</span><span class=config-val>' + v + '</span></div>';
}).join('');
var history = await fetchJSON('/api/history');
var hl = document.getElementById('history-list');
if (!history || history.length === 0) {
hl.innerHTML = '<div class=no-data>No events yet</div>';
} else {
hl.innerHTML = history.slice().reverse().map(function(e) {
var pcls = 'p' + e.priority;
var ts = new Date(e.timestamp).toLocaleTimeString();
var safeMsg = e.message.replace(/</g, '&lt;').replace(/>/g, '&gt;').substring(0, 150);
return '<div class=history-item><div class=history-meta><span>' + ts + '</span><span>' + e.source + ':' + e.name + '</span><span class="priority-tag ' + pcls + '">P' + e.priority + '</span><span> &rarr; ' + e.topic + '</span></div><div class=history-msg>' + safeMsg + '</div></div>';
}).join('');
}
} catch (e) {
console.error(e);
}
}
load();
setInterval(load, 15000);
</script>
</body>
</html>