<a href="/aliases" class="btn btn-primary active">Alias</a>
<a href="/leases" class="btn btn-primary"> DHCP Leases</a>
<a href="/devices" class="btn btn-primary"> Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary"> Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
<a href="/aliases" class="btn btn-primary active">Alias</a>
<a href="/leases" class="btn btn-primary"> DHCP Leases</a>
<a href="/devices" class="btn btn-primary"> Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary"> Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
<a href="/aliases" class="btn btn-primary"> Alias</a>
<a href="/leases" class="btn btn-primary"> DHCP Leases</a>
<a href="/devices" class="btn btn-primary active">Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary"> Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
<a href="/aliases" class="btn btn-primary"> Alias</a>
<a href="/leases" class="btn btn-primary"> DHCP Leases</a>
<a href="/devices" class="btn btn-primary active">Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary"> Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
<a href="/aliases" class="btn btn-primary"> Alias</a>
<a href="/leases" class="btn btn-primary"> DHCP Leases</a>
<a href="/devices" class="btn btn-primary"> Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary"> Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
<a href="/aliases" class="btn btn-primary"> Alias</a>
<a href="/leases" class="btn btn-primary"> DHCP Leases</a>
<a href="/devices" class="btn btn-primary"> Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary"> Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
const loader = document.getElementById("loader");
let selectedLogType = "app";
let live = true;
+// concurrent requests
let requestId = 0;
let loading = false;
+// selection
let selectedRows = new Set();
let lastSelectedRowId = null;
+// search
+let searchText = "";
+let levelFilters = new Set();
+// scroll
+let savedScrollPosition = null;
+
+// -----------------------------
+// manage the live stream
+// -----------------------------
+function handleLiveToggle() {
+ const btn = document.querySelector('[data-action="liveToggle"]');
+ const icon = btn.querySelector('i');
+
+ live = !live;
+
+ btn.classList.toggle('btn-success', live);
+ btn.classList.toggle('btn-outline-secondary', !live);
+
+ icon.className = live
+ ? 'bi bi-broadcast'
+ : 'bi bi-pause-circle';
+}
+
+// -----------------------------
+// apply filter
+// -----------------------------
+function applyLogFilters() {
+
+ const rows = document.querySelectorAll(".log-row");
+
+ rows.forEach(row => {
+
+ const text = row.textContent.toLowerCase();
+
+ const level = row.dataset.level;
+
+ const matchesText =
+ !searchText ||
+ text.includes(searchText);
+
+ const matchesLevel =
+ levelFilters.size === 0 ||
+ levelFilters.has(level);
+
+ row.style.display =
+ matchesText && matchesLevel
+ ? ""
+ : "none";
+ });
+}
+
+// -----------------------------
+// update filter button
+// -----------------------------
+function updateFilterButton() {
+ const btn = document.getElementById("filterBtn");
+ if (!btn) return;
+
+ const icon = btn.querySelector("i");
+
+ const active =
+ searchText.length > 0 ||
+ levelFilters.size > 0;
+
+ icon.className = active
+ ? "bi bi-funnel-fill"
+ : "bi bi-sliders";
+}
+
+// -----------------------------
+// clear filter
+// -----------------------------
+function clearFilters() {
+ searchText = "";
+ levelFilters.clear();
+
+ const searchInput =
+ document.getElementById("logSearch");
+
+ if (searchInput) {
+ searchInput.value = "";
+ }
+
+ document
+ .querySelectorAll(".level-filter")
+ .forEach(cb => cb.checked = false);
+
+ applyLogFilters();
+ updateFilterButton();
+
+ requestAnimationFrame(() => {
+ logViewer.scrollTop = savedScrollPosition;
+ });
+}
// -----------------------------
// load logs from backend
// shows logs in the table
// -----------------------------
function renderLogs(text, isNearBottom, previousScroll, previousHeight) {
-
const lines = text.split("\n");
const fragment = document.createDocumentFragment();
// define rowID as combination of the log
const rowId = `${date}|${level}|${source}|${msg}`;
+ // add field for selection and search
div.dataset.index = index;
div.dataset.rowId = rowId;
+ div.dataset.level = level;
+ div.dataset.source = source;
div.classList.add("log-row");
const rows = Array.from(
document.querySelectorAll(".log-row")
+ ).filter(
+ row => row.style.display !== "none"
);
- const currentIndex = Number(div.dataset.index);
-
const lastRow = rows.find(
r => r.dataset.rowId === lastSelectedRowId
);
if (lastRow) {
- const lastIndex = Number(lastRow.dataset.index);
+ const currentIndex = rows.indexOf(div);
+ const lastIndex = rows.indexOf(lastRow);
const start = Math.min(lastIndex, currentIndex);
const end = Math.max(lastIndex, currentIndex);
logViewer.innerHTML = "";
logViewer.appendChild(fragment);
+ // apply current filter
+ applyLogFilters();
if (isNearBottom) {
// "tail -f"
const newHeight = logViewer.scrollHeight;
if (newHeight < previousHeight) {
// log rotation o reset
- logViewer.scrollTop = 0;
+ logViewer.scrollTop = Math.min(previousScroll, newHeight);
} else {
const diff = newHeight - previousHeight;
logViewer.scrollTop = previousScroll + diff;
// Action Handlers
// -----------------------------
const actionHandlers = {
+ // Live Stream
+ liveToggle: (e, el) => {
+ handleLiveToggle();
+ },
// Refresh Logs
refreshLogs: (e, el) => {
loadLogs();
//},
// Reload DNS
reloadDns: async (e, el) => {
+ showToast("DNS is reloading...", true);
await handleReload(
el,
serviceReloadDNS,
},
// Reload DHCP
reloadDhcp: async (e, el) => {
+ showToast("DHCP is reloading...", true);
await handleReload(
el,
serviceReloadDHCP,
"Reloading DHCP..."
);
},
- // Reload DHCP
+ // Reload App
restartApp: async (e, el) => {
await handleRestartApp(el)
},
initEvents();
initDropdown();
+ initFilters();
// auto refresh (tipo tail -f)
setInterval(() => {
// GLOBAL EVENTS INIT
// -----------------------------
function initEvents() {
-
+ // Click
document.addEventListener('click', handleActionClick);
- const liveToggle = document.getElementById("liveToggle");
- if (liveToggle) {
- liveToggle.addEventListener("change", (e) => {
- live = e.target.checked;
- });
- }
+ // Esc button
+ document.addEventListener('keydown', e => {
+ if (e.key === 'Escape') {
+ clearFilters();
+ }
+ });
}
// -----------------------------
});
}
+
+// -----------------------------
+// Init filters
+// -----------------------------
+function initFilters() {
+
+ const searchInput =
+ document.getElementById("logSearch");
+
+ if (searchInput) {
+ searchInput.addEventListener("input", e => {
+ if (!searchText) {
+ savedScrollPosition = logViewer.scrollTop;
+ }
+ searchText = e.target.value.trim().toLowerCase();
+ applyLogFilters();
+ updateFilterButton();
+ });
+ }
+
+ document
+ .querySelectorAll(".level-filter")
+ .forEach(cb => {
+ cb.addEventListener("change", () => {
+ if (levelFilters.size === 0 && searchText.length === 0 ) {
+ savedScrollPosition = logViewer.scrollTop;
+ }
+ levelFilters.clear();
+ document
+ .querySelectorAll(".level-filter:checked")
+ .forEach(x => levelFilters.add(x.value));
+ applyLogFilters();
+ updateFilterButton();
+ });
+ });
+
+ updateFilterButton();
+}
\ No newline at end of file
// Import common js
import { loadModals, showToast, handleSearch, clearSearch, showConfirmModal, handleReload } from './common.js';
// Import services
-import { serviceGetConfigs, serviceGetConfig, serviceUpdateConfig, serviceResetConfig, serviceRestartApp, serviceIsAlive } from './services.js';
+import { serviceGetConfigs, serviceGetConfig, serviceUpdateConfig, serviceResetConfig, serviceReloadDNS, serviceReloadDHCP, serviceRestartApp, serviceIsAlive } from './services.js';
// -----------------------------
// State variables
edit: () => {
// handled by bootstrap modal show event
},
+ reloadDns: async (e, el) => {
+ showToast("DNS is reloading...", true);
+ await handleReload(
+ el,
+ serviceReloadDNS,
+ "DNS reload successfully",
+ "Error reloading DNS",
+ "Reloading DNS..."
+ );
+ },
+ // Reload DHCP
+ reloadDhcp: async (e, el) => {
+ showToast("DHCP is reloading...", true);
+ await handleReload(
+ el,
+ serviceReloadDHCP,
+ "DHCP reload successfully",
+ "Error reloading DHCP",
+ "Reloading DHCP..."
+ );
+ },
// Reload App
restartApp: async (e, el) => {
await handleRestartApp(el)
},
-}
+};
// -----------------------------
// DOMContentLoaded: bootstrap app
<a href="/aliases" class="btn btn-primary"> Alias</a>
<a href="/leases" class="btn btn-primary active">DHCP Leases</a>
<a href="/devices" class="btn btn-primary"> Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary"> Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
<a href="/aliases" class="btn btn-primary"> Alias</a>
<a href="/leases" class="btn btn-primary active">DHCP Leases</a>
<a href="/devices" class="btn btn-primary"> Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary"> Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
<!-- Bottoni -->
<div class="col-auto col-md-auto d-flex align-items-center gap-2 flex-nowrap">
- <!-- Live Flag -->
- <div class="gap-2">
- <input type="checkbox" id="liveToggle" checked> Live
+ <!-- View Panel -->
+ <div id="viewDropdown" class="dropdown">
+ <button
+ id="viewBtn"
+ class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2"
+ data-bs-toggle="dropdown">
+ <i class="bi bi-eye"></i>
+ <span class="label d-none d-md-inline">View</span>
+ </button>
+
+ <ul class="dropdown-menu dropdown-menu-end shadow">
+
+ <!-- Live Flag -->
+ <li class="px-2 py-1">
+ <button
+ class="btn btn-success w-100 d-flex align-items-center gap-2"
+ title="Live Stream On"
+ aria-label="Live Stream On"
+ data-action="liveToggle">
+ <i class="bi bi-broadcast"></i>
+ <span class="label d-md-inline">Live</span>
+ </button>
+ </li>
+
+ <!-- Refresh -->
+ <li class="px-2 py-1">
+ <button
+ class="btn btn-primary w-100 d-flex align-items-center gap-2"
+ title="Refresh"
+ aria-label="Refresh"
+ data-action="refreshLogs">
+ <i class="bi bi-arrow-repeat"></i>
+ <span class="label d-md-inline">Refresh</span>
+ </button>
+ </li>
+ </ul>
</div>
- <!-- LOG TYPE -->
+ <!-- Log Type -->
<div id="logTypeDropdown" class="dropdown">
<button
id="logTypeBtn"
<i class="bi bi-file-text"></i>
<span class="label d-none d-md-inline">App</span>
</button>
- <ul class="dropdown-menu">
+ <ul class="dropdown-menu dropdown-menu-end shadow">
<li><a class="dropdown-item active" data-value="app" href="#">App</a></li>
<li><a class="dropdown-item" data-value="access" href="#">Access</a></li>
<li><a class="dropdown-item" data-value="dhcp" href="#">DHCP</a></li>
</ul>
</div>
- <!-- Refresh -->
- <button
- class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Refresh"
- aria-label="Refresh"
- data-action="refreshLogs">
- <i class="bi bi-arrow-repeat"></i>
- <span class="label">Refresh</span>
- </button>
-
- <!-- Separator -->
- <div class="vr mx-2 d-none d-md-block"></div>
-
- <!-- Reload DNS Desktop -->
- <button
- class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
- data-action="reloadDns">
- <i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DNS</span>
- </button>
+ <!-- Filter -->
+ <div class="dropdown">
+ <button
+ id="filterBtn"
+ class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2 px-3"
+ data-bs-toggle="dropdown">
+ <i class="bi bi-sliders"></i>
+ <span class="label d-none d-md-inline">Filter</span>
+ </button>
- <!-- Reload DHCP Desktop -->
- <button
- class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
- data-action="reloadDhcp">
- <i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DHCP</span>
- </button>
+ <div class="dropdown-menu dropdown-menu-end shadow p-3" id="filterDropdown">
+ <!-- Search Bar -->
+ <div class="mb-2">
+ <label class="form-label small">Search</label>
+ <input
+ type="text"
+ id="logSearch"
+ class="form-control mb-2"
+ placeholder="Search logs">
+ </div>
+
+ <!-- Separator -->
+ <hr class="dropdown-divider">
+
+ <div class="mb-2">
+ <!-- Log Level -->
+ <div class="form-check">
+ <input class="form-check-input level-filter"
+ type="checkbox"
+ value="INFO"
+ id="filterInfo">
+ <label class="form-check-label" for="filterInfo">INFO</label>
+ </div>
+
+ <div class="form-check">
+ <input class="form-check-input level-filter"
+ type="checkbox"
+ value="WARN"
+ id="filterWarn">
+ <label class="form-check-label" for="filterWarn">WARN</label>
+ </div>
+
+ <div class="form-check">
+ <input class="form-check-input level-filter"
+ type="checkbox"
+ value="ERROR"
+ id="filterError">
+ <label class="form-check-label" for="filterError">ERROR</label>
+ </div>
+ </div>
+ </div>
+ </div>
<!-- Separator -->
<div class="vr mx-2 d-none d-md-block"></div>
- <!-- Restart Desktop -->
- <button
- class="btn btn-danger d-none d-md-flex align-items-center gap-2 px-3"
- title="Restart system"
- aria-label="Restart system"
- data-action="restartApp">
- <i class="bi bi-arrow-repeat"></i>
- <span class="label">Restart</span>
- </button>
-
- <!-- Mobile Dropdown -->
- <div class="dropdown d-md-none">
+ <!-- Admin Panel -->
+ <div id="adminDropdown" class="dropdown">
<button
- class="btn btn-primary d-flex align-items-center px-3"
- type="button"
- data-bs-toggle="dropdown"
- aria-expanded="false">
- <i class="bi bi-three-dots-vertical"></i>
+ id="adminBtn"
+ class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2 px-3"
+ data-bs-toggle="dropdown">
+ <i class="bi bi-gear"></i>
+ <span class="label d-none d-md-inline">Admin</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
- <!-- Reload DNS Mobile -->
+ <!-- Reload DNS -->
<li class="px-2 py-1">
<button
- class="btn btn-primary w-100 d-flex align-items-center gap-2"
+ class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
title="Reload DNS (BIND)"
aria-label="Reload DNS"
data-action="reloadDns">
- <i class="bi bi-arrow-repeat"></i>
- <span>Reload DNS</span>
+ <i class="bi bi-arrow-clockwise"></i>
+ <span class="label d-md-inline">Reload DNS</span>
</button>
</li>
- <!-- Reload DHCP Mobile -->
+ <!-- Reload DHCP -->
<li class="px-2 py-1">
<button
- class="btn btn-primary w-100 d-flex align-items-center gap-2"
+ class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
title="Reload DHCP (Kea)"
aria-label="Reload DHCP"
data-action="reloadDhcp">
- <i class="bi bi-arrow-repeat"></i>
- <span>Reload DHCP</span>
+ <i class="bi bi-arrow-clockwise"></i>
+ <span class="label d-md-inline">Reload DHCP</span>
</button>
</li>
- <!-- Restart Mobile -->
+ <!-- Separator -->
+ <li><hr class="dropdown-divider"></li>
+
+ <!-- Restart -->
<li class="px-2 py-1">
<button
- class="btn btn-danger w-100 d-flex align-items-center gap-2"
+ class="btn btn-danger w-100 d-flex align-items-center gap-2 text-nowrap"
title="Restart system"
aria-label="Restart system"
data-action="restartApp">
- <i class="bi bi-arrow-repeat"></i>
- <span class="label">Restart</span>
+ <i class="bi bi-power"></i>
+ <span class="label d-md-inline">Restart</span>
</button>
</li>
</ul>
<th style="width: 40px;"></th>
<th>Name</th>
<th>Date</th>
- <th class="text-end">Size</th>
- <th style="width: 120px;" class="text-end">Actions</th>
+ <th>Size</th>
+ <th style="width: 130px;" class="text-center">Actions</th>
</tr>
</thead>
<tbody id="backupList">
<a href="/aliases" class="btn btn-primary"> Alias</a>
<a href="/leases" class="btn btn-primary"> DHCP Leases</a>
<a href="/devices" class="btn btn-primary"> Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary active">Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
<a href="/aliases" class="btn btn-primary"> Alias</a>
<a href="/leases" class="btn btn-primary"> DHCP Leases</a>
<a href="/devices" class="btn btn-primary"> Devices</a>
+ <a href="/logs" class="btn btn-primary"> Logs</a>
<a href="/settings" class="btn btn-primary active">Settings</a>
<button class="btn btn-primary btn-logout">Logout</button>
</div>
<!-- Separator -->
<div class="vr mx-2 d-none d-md-block"></div>
- <!-- Restart Desktop -->
- <button
- class="btn btn-danger d-none d-md-flex align-items-center gap-2 px-3"
- title="Restart system"
- aria-label="Restart system"
- data-action="restartApp">
- <i class="bi bi-arrow-repeat"></i>
- <span class="label">Restart</span>
- </button>
+ <!-- Admin Panel -->
+ <div id="adminDropdown" class="dropdown">
+ <button
+ id="adminBtn"
+ class="btn btn-primary dropdown-toggle d-none d-md-flex align-items-center gap-2 px-3"
+ data-bs-toggle="dropdown">
+ <i class="bi bi-gear"></i>
+ <span class="label d-none d-md-inline">Admin</span>
+ </button>
+
+ <ul class="dropdown-menu dropdown-menu-end shadow">
+
+ <!-- Reload DNS -->
+ <li class="px-2 py-1">
+ <button
+ class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
+ title="Reload DNS (BIND)"
+ aria-label="Reload DNS"
+ data-action="reloadDns">
+ <i class="bi bi-arrow-clockwise"></i>
+ <span class="label d-md-inline">Reload DNS</span>
+ </button>
+ </li>
+
+ <!-- Reload DHCP -->
+ <li class="px-2 py-1">
+ <button
+ class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
+ title="Reload DHCP (Kea)"
+ aria-label="Reload DHCP"
+ data-action="reloadDhcp">
+ <i class="bi bi-arrow-clockwise"></i>
+ <span class="label d-md-inline">Reload DHCP</span>
+ </button>
+ </li>
+
+ <!-- Separator -->
+ <li><hr class="dropdown-divider"></li>
+
+ <!-- Restart -->
+ <li class="px-2 py-1">
+ <button
+ class="btn btn-danger w-100 d-flex align-items-center gap-2 text-nowrap"
+ title="Restart system"
+ aria-label="Restart system"
+ data-action="restartApp">
+ <i class="bi bi-power"></i>
+ <span class="label d-md-inline">Restart</span>
+ </button>
+ </li>
+ </ul>
+ </div>
<!-- Mobile Dropdown -->
<div class="dropdown d-md-none">
</button>
</li>
+ <!-- Separator -->
+ <li><hr class="dropdown-divider"></li>
+
+ <!-- Reload DNS Mobile -->
+ <li class="px-2 py-1">
+ <button
+ class="btn btn-primary w-100 d-flex align-items-center gap-2"
+ title="Reload DNS (BIND)"
+ aria-label="Reload DNS"
+ data-action="reloadDns">
+ <i class="bi bi-arrow-clockwise"></i>
+ <span class="label d-md-inline">Reload DNS</span>
+ </button>
+ </li>
+
+ <!-- Reload DHCP Mobile -->
+ <li class="px-2 py-1">
+ <button
+ class="btn btn-primary w-100 d-flex align-items-center gap-2"
+ title="Reload DHCP (Kea)"
+ aria-label="Reload DHCP"
+ data-action="reloadDhcp">
+ <i class="bi bi-arrow-clockwise"></i>
+ <span class="label d-md-inline">Reload DHCP</span>
+ </button>
+ </li>
+
+ <!-- Separator -->
+ <li><hr class="dropdown-divider"></li>
+
<!-- Restart Mobile -->
<li class="px-2 py-1">
<button