Topbar auf globalen Prüf-Timer reduziert, Aktionen+Beschreibung über Tabellen, sortierbare Listen
- Topbar zeigt rechts jetzt ausschließlich den "Nächste Prüfung"-Countdown, konsistent auf jeder Seite (auch anonymes Dashboard) statt nur auf dem Dashboard. Neuer Context-Processor inject_check_timer()/get_last_run_at() liefert last_run/interval global, ohne dass jede Route das selbst berechnen muss. - "+ Neu ..."-Buttons (Devices, Switches, Zugangsdaten, Benutzer, Gruppen) aus der Topbar entfernt und stattdessen in einen .section-head direkt über der jeweiligen Tabelle verschoben, zusammen mit einer kurzen Beschreibung der Seite (bisher nur bei Zugangsdaten/Gruppen vorhanden, jetzt auch bei Geräte/Switche/Benutzer). - Live-Log: eigene lokale Timer-Pill entfernt (redundant zum globalen Timer), "Aktualisieren"-Button in denselben section-head verschoben. Dashboard: Suchfeld aus der Topbar in den Seiteninhalt verschoben, lokale Timer-Anzeige entfernt (übernimmt die globale Topbar-Pill), Reload-bei- Intervallende-Logik bleibt als separater, unsichtbarer Scheduler erhalten. - Neue generische Tabellen-Sortierung (app.js: initSortableTables): Klick auf eine Spaltenüberschrift mit data-sort-key sortiert die Zeilen anhand von data-sort-<key>-Attributen. Unterstützt auch Akkordeon-Tabellen mit mehreren <tbody> (Gruppen: Haupt- + Detail-Zeile bleiben als Einheit zusammen, die virtuelle "Admin"-Zeile bleibt über data-sort-pinned immer oben). Angewendet auf Geräte, Switche, Zugangsdaten, Benutzer, Gruppen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -671,6 +671,22 @@ table.data-table {
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
.data-table thead th.sortable-col {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.data-table thead th.sortable-col:hover { color: var(--text); }
|
||||
.data-table thead th.sortable-col::after {
|
||||
content: "⇅";
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
font-size: 10px;
|
||||
opacity: 0.35;
|
||||
}
|
||||
.data-table thead th.sortable-col.sort-asc::after { content: "▲"; opacity: 0.85; }
|
||||
.data-table thead th.sortable-col.sort-desc::after { content: "▼"; opacity: 0.85; }
|
||||
|
||||
.data-table thead th {
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
|
||||
@@ -215,6 +215,81 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- Globaler "Nächste Prüfung"-Timer (Topbar, jede Seite) ---------------- */
|
||||
|
||||
function initCheckTimer() {
|
||||
const pill = document.getElementById("global-timer-pill");
|
||||
const timerEl = document.getElementById("global-timer");
|
||||
if (!pill || !timerEl) return;
|
||||
const lastRunMs = parseInt(pill.dataset.lastRunMs, 10);
|
||||
const intervalMs = parseInt(pill.dataset.intervalMs, 10);
|
||||
if (!intervalMs) { timerEl.textContent = "--"; return; }
|
||||
|
||||
function update() {
|
||||
let remainingMs = intervalMs;
|
||||
if (!isNaN(lastRunMs) && lastRunMs > 0) {
|
||||
const elapsed = Date.now() - lastRunMs;
|
||||
remainingMs = intervalMs - (((elapsed % intervalMs) + intervalMs) % intervalMs);
|
||||
}
|
||||
timerEl.textContent = Math.max(0, Math.ceil(remainingMs / 1000));
|
||||
}
|
||||
update();
|
||||
setInterval(update, 1000);
|
||||
}
|
||||
|
||||
/* ---------------- Sortierbare Tabellen ---------------- */
|
||||
/*
|
||||
* Klick auf <th data-sort-key="..."> sortiert die Tabelle. Zwei Modi:
|
||||
* - Normale Tabellen: sortiert <tr>-Zeilen innerhalb des einzigen <tbody>
|
||||
* anhand von data-sort-<key> auf der jeweiligen <tr>.
|
||||
* - Akkordeon-Tabellen (mehrere <tbody>, z.B. Gruppen mit Detail-Zeile):
|
||||
* sortiert ganze <tbody>-Blöcke anhand von data-sort-<key> auf dem
|
||||
* jeweiligen <tbody>, damit Haupt- und Detail-Zeile zusammenbleiben.
|
||||
* <tbody data-sort-pinned> (z.B. die virtuelle "Admin"-Zeile) bleibt
|
||||
* dabei immer an ihrer Position.
|
||||
*/
|
||||
function compareSortValues(a, b, asc) {
|
||||
a = (a === null || a === undefined) ? "" : String(a);
|
||||
b = (b === null || b === undefined) ? "" : String(b);
|
||||
const na = parseFloat(a), nb = parseFloat(b);
|
||||
const bothNumeric = a !== "" && b !== "" && !isNaN(na) && !isNaN(nb);
|
||||
const cmp = bothNumeric ? (na - nb) : a.toLowerCase().localeCompare(b.toLowerCase(), "de");
|
||||
return asc ? cmp : -cmp;
|
||||
}
|
||||
|
||||
function sortTable(table, th) {
|
||||
const key = th.dataset.sortKey;
|
||||
const asc = th.dataset.sortDir !== "asc";
|
||||
table.querySelectorAll("thead th[data-sort-key]").forEach((h) => {
|
||||
delete h.dataset.sortDir;
|
||||
h.classList.remove("sort-asc", "sort-desc");
|
||||
});
|
||||
th.dataset.sortDir = asc ? "asc" : "desc";
|
||||
th.classList.add(asc ? "sort-asc" : "sort-desc");
|
||||
|
||||
if (table.tBodies.length > 1) {
|
||||
const movable = Array.prototype.filter.call(table.tBodies, (tb) => !tb.hasAttribute("data-sort-pinned"));
|
||||
movable.sort((a, b) => compareSortValues(
|
||||
a.getAttribute("data-sort-" + key), b.getAttribute("data-sort-" + key), asc
|
||||
));
|
||||
movable.forEach((tb) => table.appendChild(tb));
|
||||
} else {
|
||||
const tbody = table.tBodies[0];
|
||||
const rows = Array.prototype.filter.call(tbody.rows, (r) => !r.classList.contains("empty-row"));
|
||||
rows.sort((a, b) => compareSortValues(
|
||||
a.getAttribute("data-sort-" + key), b.getAttribute("data-sort-" + key), asc
|
||||
));
|
||||
rows.forEach((r) => tbody.appendChild(r));
|
||||
}
|
||||
}
|
||||
|
||||
function initSortableTables() {
|
||||
document.querySelectorAll("table[data-sortable] thead th[data-sort-key]").forEach((th) => {
|
||||
th.classList.add("sortable-col");
|
||||
th.addEventListener("click", () => sortTable(th.closest("table"), th));
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- Init ---------------- */
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
@@ -224,6 +299,8 @@
|
||||
initConfirmables();
|
||||
initFlashedMessages();
|
||||
initNavGroups();
|
||||
initCheckTimer();
|
||||
initSortableTables();
|
||||
document.querySelectorAll("[data-theme-toggle]").forEach((btn) => btn.addEventListener("click", toggleTheme));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user