fa317a3b78
Theme preference stored in localStorage (auto follows the OS setting). The chosen data-theme attribute is applied synchronously in <head> to avoid any flash of unstyled content. CSS custom properties handle all surface, text, border and input colours across every page. The Appearance section on the profile page lets each user switch modes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
694 lines
23 KiB
HTML
694 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
{% include 'head.html' %}
|
|
|
|
<style>
|
|
body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
body {
|
|
height: auto;
|
|
min-height: 100vh;
|
|
overflow: auto;
|
|
flex-direction: column;
|
|
}
|
|
.container {
|
|
max-height: none;
|
|
overflow: visible;
|
|
}
|
|
.table-section {
|
|
max-height: 55vh;
|
|
}
|
|
.log-section {
|
|
flex: none;
|
|
max-height: 40vh;
|
|
}
|
|
}
|
|
|
|
.container {
|
|
flex: 1;
|
|
min-height: 0;
|
|
max-width: 1600px;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
h1 {
|
|
color: #333;
|
|
margin-bottom: 5px;
|
|
margin-top: 15px;
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
h2 {
|
|
color: #333;
|
|
margin-bottom: 10px;
|
|
font-size: 1.2em;
|
|
padding: 10px 15px;
|
|
background: white;
|
|
border-radius: 6px;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.subtitle {
|
|
color: #666;
|
|
margin-bottom: 15px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.table-section {
|
|
background: white;
|
|
border-radius: 6px;
|
|
padding: 15px;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
|
overflow-x: auto;
|
|
overflow-y: auto;
|
|
max-height: 60vh;
|
|
}
|
|
|
|
.log-section {
|
|
flex: 1;
|
|
min-height: 0;
|
|
background: white;
|
|
border-radius: 6px;
|
|
padding: 15px;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
#ntable {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
#ntable td,
|
|
#ntable th {
|
|
border: 1px solid #e0e0e0;
|
|
text-align: left;
|
|
padding: 2px 4px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#ntable tr:nth-child(even) {
|
|
background-color: #fafafa;
|
|
}
|
|
|
|
#ntable tr:hover {
|
|
background-color: #e3f2fd;
|
|
}
|
|
|
|
#ntable tbody tr.row-warning {
|
|
background-color: #fff8c5;
|
|
}
|
|
|
|
#ntable tbody tr.row-critical {
|
|
background-color: #fde8e8;
|
|
}
|
|
|
|
#ntable tbody tr.row-warning:hover {
|
|
background-color: #fff0a0;
|
|
}
|
|
|
|
#ntable tbody tr.row-critical:hover {
|
|
background-color: #f9c8c8;
|
|
}
|
|
|
|
#ntable th {
|
|
padding: 6px 8px;
|
|
background-color: #2196f3;
|
|
color: white;
|
|
font-weight: 600;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
#ntable
|
|
th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):after {
|
|
content: " ⇅";
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Alert count column styling */
|
|
#ntable td.alert-warning {
|
|
color: #ff9800;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
|
|
#ntable td.alert-critical {
|
|
color: #f44336;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Scrollbar styling */
|
|
.log-section::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.log-section::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.log-section::-webkit-scrollbar-thumb {
|
|
background: #888;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.log-section::-webkit-scrollbar-thumb:hover {
|
|
background: #555;
|
|
}
|
|
|
|
/* Message styling */
|
|
#messages {
|
|
font-size: 1.00em;
|
|
line-height: 1.0;
|
|
}
|
|
|
|
#messages .log-entry {
|
|
padding: 5px 0;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
display: flex;
|
|
gap: 0.5em;
|
|
align-items: baseline;
|
|
}
|
|
|
|
.log-ts { color: #888; white-space: nowrap; }
|
|
.log-level { font-weight: bold; min-width: 6em; }
|
|
.log-host { font-weight: 600; }
|
|
.log-service { color: #888; }
|
|
|
|
.log-warning .log-level { color: #b8860b; }
|
|
.log-critical .log-level { color: #c00; }
|
|
.log-recover .log-level { color: #2a7a2a; }
|
|
.log-info .log-level { color: #555; }
|
|
|
|
.log-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 10px;
|
|
background: white;
|
|
border-radius: 6px;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
|
padding: 8px 15px;
|
|
}
|
|
|
|
.log-section-title {
|
|
font-size: 1.2em;
|
|
font-weight: bold;
|
|
color: #333;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.log-filter-bar {
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.log-filter-bar input[type="text"],
|
|
.log-filter-bar select {
|
|
padding: 3px 7px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
font-size: 1.00em;
|
|
color: #333;
|
|
}
|
|
|
|
.log-filter-bar input[type="text"] { width: 110px; }
|
|
|
|
/* Modal for connection status messages */
|
|
.connection-modal {
|
|
display: none;
|
|
position: fixed;
|
|
z-index: 1000;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.connection-modal.show {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.connection-modal-content {
|
|
background-color: white;
|
|
padding: 30px 40px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
min-width: 300px;
|
|
}
|
|
|
|
.connection-modal-content p {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
color: #333;
|
|
}
|
|
|
|
/* State indicators */
|
|
.state-up {
|
|
color: #4caf50;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.state-down {
|
|
color: #f44336;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.state-overdue {
|
|
color: #ff9800;
|
|
font-weight: 700;
|
|
}
|
|
#ntable a.host-link { color: inherit; text-decoration: none; }
|
|
#ntable a.host-link:hover { text-decoration: underline; }
|
|
|
|
/* ── Dark mode ── */
|
|
html[data-theme="dark"] h1,
|
|
html[data-theme="dark"] h2 { color: var(--text); }
|
|
html[data-theme="dark"] .subtitle { color: var(--text-sec); }
|
|
html[data-theme="dark"] h2,
|
|
html[data-theme="dark"] .table-section,
|
|
html[data-theme="dark"] .log-section,
|
|
html[data-theme="dark"] .log-section-header { background: var(--surface); }
|
|
html[data-theme="dark"] .log-section-title { color: var(--text); }
|
|
html[data-theme="dark"] #ntable td,
|
|
html[data-theme="dark"] #ntable th { border-color: var(--border); }
|
|
html[data-theme="dark"] #ntable tr:nth-child(even) { background: var(--surface-2); }
|
|
html[data-theme="dark"] #ntable tr:hover { background: #1e3a5f; }
|
|
html[data-theme="dark"] #ntable tbody tr.row-warning { background: #3a2800; }
|
|
html[data-theme="dark"] #ntable tbody tr.row-critical { background: #3a0a0a; }
|
|
html[data-theme="dark"] #ntable tbody tr.row-warning:hover { background: #4a3200; }
|
|
html[data-theme="dark"] #ntable tbody tr.row-critical:hover { background: #4a1010; }
|
|
html[data-theme="dark"] #messages .log-entry { border-bottom-color: var(--border-3); }
|
|
html[data-theme="dark"] .log-ts,
|
|
html[data-theme="dark"] .log-service { color: var(--text-muted); }
|
|
html[data-theme="dark"] .log-info .log-level { color: var(--text-sec); }
|
|
html[data-theme="dark"] .log-filter-bar input,
|
|
html[data-theme="dark"] .log-filter-bar select { color: var(--text); }
|
|
html[data-theme="dark"] .connection-modal-content { background: var(--surface); color: var(--text); }
|
|
</style>
|
|
<script type="text/javascript">
|
|
var cnt = 0;
|
|
var nTable = document;
|
|
var name_idx = {};
|
|
var c = 0;
|
|
var HBD_VERSION = "{{ hbd_version }}";
|
|
|
|
function hostNameHtml(data) {
|
|
var rawName = data.raw_name || data.name.replace(/<[^>]+>/g, '').replace('*', '').trim();
|
|
var nameHtml = data.name;
|
|
if (!data.hbc_version || data.hbc_version !== HBD_VERSION) {
|
|
nameHtml += ' 🥀';
|
|
}
|
|
var display = data.dyn ? '<b>' + nameHtml + '</b>' : nameHtml;
|
|
return '<a class="host-link" href="/plugins#' + encodeURIComponent(rawName) + '">' + display + '</a>';
|
|
}
|
|
|
|
function setup() {
|
|
name_idx = {};
|
|
nTable = document.getElementById("ntable");
|
|
for (var i = 0, row; (row = nTable.rows[i]); i++) {
|
|
if (i == 0) continue;
|
|
var cell = nTable.rows[i].cells[0];
|
|
var name = cell.dataset.name || cell.innerText.replace(/\s*🥀\s*$/, '').trim();
|
|
name_idx[name] = nTable.rows[i];
|
|
}
|
|
}
|
|
|
|
function updateRowAlert(row, data) {
|
|
var criticalUnacked = data.alert_critical_unacked || 0;
|
|
var criticalAcked = data.alert_critical_acked || 0;
|
|
var warningUnacked = data.alert_warning_unacked || 0;
|
|
var warningAcked = data.alert_warning_acked || 0;
|
|
row.classList.remove('row-warning', 'row-critical');
|
|
if (criticalUnacked > 0 || criticalAcked > 0) {
|
|
row.classList.add('row-critical');
|
|
} else if (warningUnacked > 0 || warningAcked > 0) {
|
|
row.classList.add('row-warning');
|
|
}
|
|
}
|
|
|
|
function createRow(data) {
|
|
var row = document.createElement("tr");
|
|
var c_name = document.createElement("td");
|
|
var c_warning = document.createElement("td");
|
|
c_warning.style.textAlign = "center";
|
|
c_warning.style.color = "#ff9800";
|
|
c_warning.style.fontWeight = "bold";
|
|
var c_critical = document.createElement("td");
|
|
c_critical.style.textAlign = "center";
|
|
c_critical.style.color = "#f44336";
|
|
c_critical.style.fontWeight = "bold";
|
|
var c_ipv4addr = document.createElement("td");
|
|
var c_ipv4state = document.createElement("td");
|
|
var c_ipv4latency = document.createElement("td");
|
|
c_ipv4latency.style.textAlign = "right";
|
|
var c_ipv4statets = document.createElement("td");
|
|
c_ipv4statets.style.textAlign = "right";
|
|
var c_ipv6addr = document.createElement("td");
|
|
var c_ipv6state = document.createElement("td");
|
|
var c_ipv6latency = document.createElement("td");
|
|
c_ipv6latency.style.textAlign = "right";
|
|
var c_ipv6statets = document.createElement("td");
|
|
c_ipv6statets.style.textAlign = "right";
|
|
row.appendChild(c_name);
|
|
row.appendChild(c_warning);
|
|
row.appendChild(c_critical);
|
|
row.appendChild(c_ipv4addr);
|
|
row.appendChild(c_ipv4state);
|
|
row.appendChild(c_ipv4latency);
|
|
row.appendChild(c_ipv4statets);
|
|
row.appendChild(c_ipv6addr);
|
|
row.appendChild(c_ipv6state);
|
|
row.appendChild(c_ipv6latency);
|
|
row.appendChild(c_ipv6statets);
|
|
c_name.dataset.name = data.name;
|
|
c_name.innerHTML = hostNameHtml(data);
|
|
|
|
// Set alert counts in "x/y" format (unacked/acked)
|
|
var warningUnacked = data.alert_warning_unacked || 0;
|
|
var warningAcked = data.alert_warning_acked || 0;
|
|
var criticalUnacked = data.alert_critical_unacked || 0;
|
|
var criticalAcked = data.alert_critical_acked || 0;
|
|
|
|
if (warningUnacked > 0 || warningAcked > 0) {
|
|
c_warning.innerHTML = warningAcked > 0 ? warningUnacked + "/" + warningAcked : warningUnacked;
|
|
} else {
|
|
c_warning.innerHTML = "";
|
|
}
|
|
|
|
if (criticalUnacked > 0 || criticalAcked > 0) {
|
|
c_critical.innerHTML = criticalAcked > 0 ? criticalUnacked + "/" + criticalAcked : criticalUnacked;
|
|
} else {
|
|
c_critical.innerHTML = "";
|
|
}
|
|
|
|
c_ipv4addr.innerHTML = data.connections[0].addr;
|
|
c_ipv4state.innerHTML = data.connections[0].state;
|
|
if (data.connections.length > 1) {
|
|
c_ipv6addr.innerHTML = data.connections[1].addr;
|
|
c_ipv6state.innerHTML = data.connections[1].state;
|
|
}
|
|
var table = document.getElementById("ntablebody"); // find table to append to
|
|
table.appendChild(row); // append row to table
|
|
name_idx[c_name] = row;
|
|
updateRowAlert(row, data);
|
|
}
|
|
|
|
function formatTS(ts) {
|
|
const now = new Date();
|
|
const d = new Date(ts * 1000);
|
|
|
|
const pad = n => String(n).padStart(2, '0');
|
|
const timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
|
|
// Same calendar day → show time only
|
|
if (d.toDateString() === now.toDateString()) {
|
|
return timeStr;
|
|
}
|
|
|
|
// Within 8 days → show "-X d hh:mm:ss"
|
|
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
const dStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
const diffDays = Math.round((todayStart - dStart) / 86400000);
|
|
if (diffDays < 8) {
|
|
return `-${diffDays}d ${timeStr}`;
|
|
}
|
|
|
|
// Older → date only
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
}
|
|
|
|
function update_table(data) {
|
|
if (!(data.name in name_idx)) {
|
|
createRow(data);
|
|
setup();
|
|
}
|
|
|
|
// Update name cell (version indicator)
|
|
var nameCell = name_idx[data.name].cells[0];
|
|
nameCell.dataset.name = data.name;
|
|
nameCell.innerHTML = hostNameHtml(data);
|
|
|
|
// Update warning and critical counts in "x/y" format (unacked/acked)
|
|
var warningUnacked = data.alert_warning_unacked || 0;
|
|
var warningAcked = data.alert_warning_acked || 0;
|
|
var criticalUnacked = data.alert_critical_unacked || 0;
|
|
var criticalAcked = data.alert_critical_acked || 0;
|
|
|
|
if (warningUnacked > 0 || warningAcked > 0) {
|
|
name_idx[data.name].cells[1].innerHTML = warningAcked > 0 ? warningUnacked + "/" + warningAcked : warningUnacked;
|
|
} else {
|
|
name_idx[data.name].cells[1].innerHTML = "";
|
|
}
|
|
|
|
if (criticalUnacked > 0 || criticalAcked > 0) {
|
|
name_idx[data.name].cells[2].innerHTML = criticalAcked > 0 ? criticalUnacked + "/" + criticalAcked : criticalUnacked;
|
|
} else {
|
|
name_idx[data.name].cells[2].innerHTML = "";
|
|
}
|
|
|
|
for (var i = 0; i < data.connections.length; i++) {
|
|
// Offset by 2 for the warning/critical count columns
|
|
name_idx[data.name].cells[3 + i * 4].innerHTML = data.connections[i].addr;
|
|
name_idx[data.name].cells[6 + i * 4].innerHTML = formatTS(
|
|
data.connections[i].statetime
|
|
);
|
|
if (data.connections[i].state == "up") {
|
|
state = '<span class="state-up">up</span>';
|
|
latency = String(Math.round(Number.parseFloat(data.connections[i].rtts[0])));
|
|
} else {
|
|
if (data.connections[i].state == "unknown") {
|
|
state = "";
|
|
latency = "";
|
|
name_idx[data.name].cells[3 + i * 4].innerHTML = "";
|
|
name_idx[data.name].cells[6 + i * 4].innerHTML = "";
|
|
} else if (data.connections[i].state == "down") {
|
|
state = '<span class="state-down">down</span>';
|
|
latency = "-";
|
|
} else if (data.connections[i].state == "overdue") {
|
|
state = '<span class="state-overdue">overdue</span>';
|
|
latency = "-";
|
|
} else {
|
|
state = "<b>" + data.connections[i].state + "</b>";
|
|
latency = "-";
|
|
}
|
|
}
|
|
name_idx[data.name].cells[4 + i * 4].innerHTML = state;
|
|
name_idx[data.name].cells[5 + i * 4].innerHTML = latency;
|
|
}
|
|
updateRowAlert(name_idx[data.name], data);
|
|
}
|
|
|
|
function applyLogFilters() {
|
|
var hostFilter = document.getElementById('filter-host').value.toLowerCase().trim();
|
|
var levelFilter = document.getElementById('filter-level').value;
|
|
var msgFilter = document.getElementById('filter-msg').value.toLowerCase().trim();
|
|
document.querySelectorAll('#messages .log-entry').forEach(function(entry) {
|
|
var show = true;
|
|
if (hostFilter && !(entry.dataset.host || '').toLowerCase().includes(hostFilter)) show = false;
|
|
if (levelFilter && entry.dataset.level !== levelFilter) show = false;
|
|
if (msgFilter) {
|
|
var msgEl = entry.querySelector('.log-msg');
|
|
if (!msgEl || !msgEl.textContent.toLowerCase().includes(msgFilter)) show = false;
|
|
}
|
|
entry.style.display = show ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
function WS_Connect() {
|
|
if ("WebSocket" in window) {
|
|
//N.B: subprotocol field causes chrome to error 1006
|
|
var ws_hbd = new WebSocket("{{heartbeat_ws_url}}", /* "hdb" */ );
|
|
|
|
ws_hbd.onopen = function () {
|
|
// Web Socket is connected, send data using send()
|
|
console.log("ws connect {{heartbeat_ws_url}}");
|
|
// Hide modal window if visible
|
|
var modal = document.getElementById("connectionModal");
|
|
if (modal) {
|
|
modal.classList.remove("show");
|
|
}
|
|
ws_hbd.send("heartbeat_web");
|
|
};
|
|
|
|
ws_hbd.onerror = function (event) {
|
|
console.log(event);
|
|
};
|
|
|
|
ws_hbd.onmessage = function (event) {
|
|
/* console.log(event.data); */
|
|
var state = JSON.parse(event.data);
|
|
/* console.log("State: " + state.type); */
|
|
if (state.type == "host") {
|
|
update_table(state.data);
|
|
} else if (state.type == "message") {
|
|
var msgs = document.getElementById("messages");
|
|
var msg = state.data;
|
|
var _d = new Date(msg.ts * 1000);
|
|
function _p(n) { return n < 10 ? '0' + n : '' + n; }
|
|
var ts_str = _d.getFullYear() + '-' + _p(_d.getMonth()+1) + '-' + _p(_d.getDate())
|
|
+ ' ' + _p(_d.getHours()) + ':' + _p(_d.getMinutes()) + ':' + _p(_d.getSeconds());
|
|
var lvl = (msg.level || "INFO").toLowerCase();
|
|
var hostVal = msg.host || '';
|
|
var html = '<div class="log-entry log-' + lvl + '" data-level="' + lvl + '" data-host="' + hostVal.replace(/"/g, '"') + '">';
|
|
html += '<span class="log-ts">' + ts_str + '</span>';
|
|
html += '<span class="log-level">' + (msg.level || "") + '</span>';
|
|
if (msg.host) html += '<span class="log-host">' + msg.host + '</span>';
|
|
if (msg.service) html += '<span class="log-service">' + msg.service + '</span>';
|
|
html += '<span class="log-msg">' + msg.message + '</span>';
|
|
html += '</div>';
|
|
msgs.insertAdjacentHTML(state.history ? "beforeend" : "afterbegin", html);
|
|
applyLogFilters();
|
|
}
|
|
cnt++;
|
|
};
|
|
|
|
ws_hbd.onclose = function (event) {
|
|
/* console.log(event); */
|
|
console.log("Connection is closed, reopening");
|
|
// Show modal window
|
|
var modal = document.getElementById("connectionModal");
|
|
if (modal) {
|
|
modal.classList.add("show");
|
|
}
|
|
setTimeout(function () {
|
|
WS_Connect();
|
|
}, 3000);
|
|
};
|
|
} else {
|
|
// The browser doesn't support WebSocket
|
|
console.log("WebSocket NOT supported by your Browser!");
|
|
}
|
|
}
|
|
WS_Connect();
|
|
</script>
|
|
<body>
|
|
{% include 'nav.html' %}
|
|
|
|
{% include 'menu.html' %}
|
|
|
|
<div class="container">
|
|
<div>
|
|
<h1>{{ header }}</h1>
|
|
<p class="subtitle">Real-time host monitoring and event log</p>
|
|
</div>
|
|
|
|
<div class="table-section">
|
|
<table id="ntable" class="sortable">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th style="text-align: center" title="Warning Alerts">⚠️</th>
|
|
<th style="text-align: center" title="Critical Alerts">🔴</th>
|
|
<th>IPv4 Addr</th>
|
|
<th>State</th>
|
|
<th style="text-align: right">Latency</th>
|
|
<th style="text-align: right">Last State</th>
|
|
<th>IPv6 Addr</th>
|
|
<th>State</th>
|
|
<th style="text-align: right">Latency</th>
|
|
<th style="text-align: right">Last State</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="ntablebody">
|
|
{% for host in hosts %}
|
|
<tr class="{% if host.alert_critical_unacked > 0 or host.alert_critical_acked > 0 %}row-critical{% elif host.alert_warning_unacked > 0 or host.alert_warning_acked > 0 %}row-warning{% endif %}">
|
|
<td data-name="{{ host.name }}"><a class="host-link" href="/plugins#{{ host.raw_name | urlencode }}">{{ host.name }}{% if not host.hbc_version or host.hbc_version != hbd_version %} 🥀{% endif %}</a></td>
|
|
<td style="text-align: center; color: #ff9800; font-weight: bold;">
|
|
{%- set warning_unacked = host.alert_warning_unacked -%}
|
|
{%- set warning_acked = host.alert_warning_acked -%}
|
|
{%- if warning_unacked > 0 or warning_acked > 0 -%}
|
|
{{ warning_unacked }}{% if warning_acked > 0 %}/{{ warning_acked }}{% endif %}
|
|
{%- endif -%}
|
|
</td>
|
|
<td style="text-align: center; color: #f44336; font-weight: bold;">
|
|
{%- set critical_unacked = host.alert_critical_unacked -%}
|
|
{%- set critical_acked = host.alert_critical_acked -%}
|
|
{%- if critical_unacked > 0 or critical_acked > 0 -%}
|
|
{{ critical_unacked }}{% if critical_acked > 0 %}/{{ critical_acked }}{% endif %}
|
|
{%- endif -%}
|
|
</td>
|
|
{% for conn in host.connections %}
|
|
<td>{{ conn.addr if conn.addr else '' }}</td>
|
|
<td>{{ conn.state if conn.state else '' }}</td>
|
|
<td style="text-align: right">{{ conn.latency if conn.latency else '' }}</td>
|
|
<td style="text-align: right">{{ conn.last_state_ts if conn.last_state_ts else '' }}</td>
|
|
{% endfor %}
|
|
{% if host.connections|length == 0 %}
|
|
<td></td><td></td><td></td><td></td>
|
|
<td></td><td></td><td></td><td></td>
|
|
{% elif host.connections|length == 1 %}
|
|
<td></td><td></td><td></td><td></td>
|
|
{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="log-section">
|
|
<div class="log-section-header">
|
|
<span class="log-section-title">Log of Events</span>
|
|
<div class="log-filter-bar">
|
|
<input type="text" id="filter-host" placeholder="Host…" title="Filter by host" />
|
|
<select id="filter-level" title="Filter by level">
|
|
<option value="">All levels</option>
|
|
<option value="info">INFO</option>
|
|
<option value="warning">WARNING</option>
|
|
<option value="critical">CRITICAL</option>
|
|
<option value="recover">RECOVER</option>
|
|
<option value="unknown">UNKNOWN</option>
|
|
</select>
|
|
<input type="text" id="filter-msg" placeholder="Message…" title="Filter by message text" />
|
|
</div>
|
|
</div>
|
|
<div id="messages"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{% include 'foot.html' %}
|
|
|
|
<!-- Connection status modal -->
|
|
<div id="connectionModal" class="connection-modal">
|
|
<div class="connection-modal-content">
|
|
<p>⚠️ Connection is closed, reopening...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
setup();
|
|
document.getElementById('filter-host').addEventListener('input', applyLogFilters);
|
|
document.getElementById('filter-level').addEventListener('change', applyLogFilters);
|
|
document.getElementById('filter-msg').addEventListener('input', applyLogFilters);
|
|
</script>
|
|
</body>
|
|
</html>
|