Erklärende Kommentare aus dem Code entfernt, in KNOWLEDGE_BASE.md überführt
Betrifft app.py, create_db.py, poe.sh sowie alle HTML-Templates mit
Kommentaren (Jinja {# #}, HTML <!-- -->, eingebettetes JavaScript //).
Docstrings bleiben unangetastet -- nur reine Kommentarzeilen/-fragmente
wurden entfernt, damit der Code selbst schlank bleibt, ohne die
dahinterliegende Begründung zu verlieren.
Umsetzung:
- app.py/create_db.py: per Pythons eigenem tokenize-Modul entfernt (sicher
gegen '#' innerhalb von String-Literalen, Shebang-Zeile ausgenommen).
- poe.sh: manuell, da einzelne expect-Prompt-Strings ('expect "#"')
ein literales '#' als Teil des Strings enthalten, kein Kommentar.
- Templates: {# #}/<!-- --> per Regex (eindeutige Delimiter, anschließend
mit Jinja2s eigenem Parser validiert), echte // -JS-Kommentare manuell
identifiziert (zwei Fundstellen mit '//' waren tatsächlich Code --
als Protocol-relative-URL-Template-
String -- und wurden bewusst nicht angefasst).
Verifiziert: alle Python-/Bash-/Jinja-Dateien syntaktisch weiterhin
gültig, live auf der Testbox deployt und alle 14 Kernrouten mit 200
bestätigt, keine Fehler im App-Log.
This commit is contained in:
+11
-901
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,6 @@ DB_PATH = os.environ.get("TESM_DB_PATH", os.path.join(BASE_DIR, "sqlite.db"))
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
|
||||
# Zugangsdaten (wiederverwendbare SSH-Logins, mehrere Switche können sich
|
||||
# dieselben teilen; Passwort wird verschlüsselt gespeichert)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS credentials (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -33,8 +31,6 @@ CREATE TABLE IF NOT EXISTS credentials (
|
||||
);
|
||||
""")
|
||||
|
||||
# Switches (Aruba-Switche) — referenzieren ihre Zugangsdaten statt sie
|
||||
# direkt zu speichern
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS switches (
|
||||
hostname TEXT PRIMARY KEY,
|
||||
@@ -48,7 +44,6 @@ CREATE TABLE IF NOT EXISTS switches (
|
||||
);
|
||||
""")
|
||||
|
||||
# Devices
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
mac TEXT PRIMARY KEY,
|
||||
@@ -66,7 +61,6 @@ CREATE TABLE IF NOT EXISTS devices (
|
||||
);
|
||||
""")
|
||||
|
||||
# Benutzer
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -83,9 +77,6 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
);
|
||||
""")
|
||||
|
||||
# Gruppen (Rechtesystem) — Admins besitzen ohnehin alle Rechte; Gruppen
|
||||
# dienen dazu, normalen Benutzern gezielt einzelne Verwaltungsrechte für
|
||||
# Devices/Switches zu geben, ohne sie zu Admins zu machen.
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -96,7 +87,6 @@ CREATE TABLE IF NOT EXISTS groups (
|
||||
);
|
||||
""")
|
||||
|
||||
# Welche Berechtigungen eine Gruppe gewährt (siehe app.py: PERMISSIONS)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS group_permissions (
|
||||
group_id INTEGER NOT NULL,
|
||||
@@ -106,8 +96,6 @@ CREATE TABLE IF NOT EXISTS group_permissions (
|
||||
);
|
||||
""")
|
||||
|
||||
# Gruppenmitgliedschaft (ein Benutzer kann in mehreren Gruppen sein;
|
||||
# effektive Rechte = Vereinigung aller Gruppen)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
user_id INTEGER NOT NULL,
|
||||
@@ -118,7 +106,6 @@ CREATE TABLE IF NOT EXISTS user_groups (
|
||||
);
|
||||
""")
|
||||
|
||||
# Settings (z.B. Prüfintervall)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
@@ -126,11 +113,6 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
);
|
||||
""")
|
||||
|
||||
# DHCP: eigene Options-Definitionen (analog "Vordefinierte Optionen und
|
||||
# Werte" bei einem Windows-DHCP-Server) — Code/Name/Typ/Beschreibung.
|
||||
# is_standard markiert die vorbefüllten Standard-Optionen (Router/DNS/…) --
|
||||
# die können vom Admin nicht gelöscht werden, nur ihr Wert geleert (die
|
||||
# tatsächliche Vorbefüllung übernimmt app.py: _ensure_schema()).
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS dhcp_option_defs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -142,12 +124,6 @@ CREATE TABLE IF NOT EXISTS dhcp_option_defs (
|
||||
);
|
||||
""")
|
||||
|
||||
# DHCP: Werte je Option — device_mac='' bedeutet global (gilt für den
|
||||
# ganzen Scope), ein gesetzter device_mac überschreibt den globalen Wert
|
||||
# für genau dieses Gerät. Bewusst '' statt NULL für den globalen Fall,
|
||||
# da SQLite NULL in UNIQUE-Constraints nicht als gleich behandelt (zwei
|
||||
# NULL-Zeilen wären sonst kein Konflikt) — mit '' funktioniert ON CONFLICT
|
||||
# zuverlässig für den Upsert des globalen Werts.
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS dhcp_option_values (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -159,11 +135,6 @@ CREATE TABLE IF NOT EXISTS dhcp_option_values (
|
||||
);
|
||||
""")
|
||||
|
||||
# DHCP: konfigurierte Subnetze (Interface + Range + optionales Gateway/DNS)
|
||||
# -- ein Host kann mehrere Subnetze gleichzeitig bedienen, je eines pro
|
||||
# Interface/Netz. sort_order steuert die Anzeige-/Verarbeitungsreihenfolge,
|
||||
# enabled erlaubt ein Subnetz vorübergehend aus der geschriebenen
|
||||
# Konfiguration auszuschließen, ohne es zu löschen.
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS dhcp_subnets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -177,9 +148,6 @@ CREATE TABLE IF NOT EXISTS dhcp_subnets (
|
||||
);
|
||||
""")
|
||||
|
||||
# DHCP: manuelle Reservierungen für Geräte OHNE eigenen Client-Datensatz
|
||||
# (Geräte mit MAC+IP in der devices-Tabelle bekommen ihre Reservierung
|
||||
# automatisch, siehe _dhcp_reservation_candidates in app.py).
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS dhcp_manual_reservations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -189,11 +157,6 @@ CREATE TABLE IF NOT EXISTS dhcp_manual_reservations (
|
||||
);
|
||||
""")
|
||||
|
||||
# Service-Accounts (aktuell nur das LDAP-Bind-Konto) -- bewusst NICHT in der
|
||||
# users-Tabelle (kein echter, einloggbarer App-Benutzer) und NICHT in der
|
||||
# credentials-Tabelle (die ist für Switch-/Geräte-Zugangsdaten, ein anderer
|
||||
# Anwendungsfall) -- eine eigene, kleine Tabelle. "purpose" ist UNIQUE, ein
|
||||
# neu gespeichertes Konto für denselben Zweck ersetzt das alte.
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS service_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -203,9 +166,6 @@ CREATE TABLE IF NOT EXISTS service_accounts (
|
||||
);
|
||||
""")
|
||||
|
||||
# AD-Gruppe -> App-Rechtegruppe: mehrere AD-Gruppen können jeweils einer
|
||||
# App-Gruppe zugeordnet werden (additiv, siehe _ldap_resolve_app_groups in
|
||||
# app.py) -- passt keine dieser Gruppen, greift die Standardgruppe.
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ldap_group_mappings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -216,8 +176,6 @@ CREATE TABLE IF NOT EXISTS ldap_group_mappings (
|
||||
);
|
||||
""")
|
||||
|
||||
# Änderungslog (Audit-Log) — wer hat wann was geändert. Bewusst OHNE
|
||||
# PoE-Neustarts (die stehen im Live-Log von poe.sh).
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -229,14 +187,9 @@ CREATE TABLE IF NOT EXISTS audit_log (
|
||||
);
|
||||
""")
|
||||
|
||||
# Standard-Setting: Prüfintervall 5 Minuten (interval = Minuten, check_interval = Sekunden)
|
||||
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("interval", "5"))
|
||||
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("check_interval", "300"))
|
||||
|
||||
# Standardgruppe 'Benutzer' — Systemgruppe (is_system), Name und Rechte
|
||||
# sind über die UI fest und nicht änderbar. Jeder neu angelegte (nicht-admin)
|
||||
# Benutzer landet automatisch darin (siehe app.py: users()). Der genaue
|
||||
# Rechtesatz muss mit DEFAULT_GROUP_PERMISSIONS in app.py übereinstimmen.
|
||||
cur = c.execute("INSERT OR IGNORE INTO groups (name, is_default, is_system) VALUES (?, 1, 1)", ("Benutzer",))
|
||||
if cur.rowcount > 0:
|
||||
default_group_id = cur.lastrowid
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
<!-- Live-Update-fähiges Fragment: wird von index.html initial eingebunden UND
|
||||
unverändert von /dashboard/tiles (AJAX) nachgeladen, damit das Dashboard
|
||||
ohne vollen Seiten-Reload aktuell bleibt (siehe dashboard-tiles.js im
|
||||
scripts-Block von index.html). -->
|
||||
<div id="dashboard-tiles" data-last-run-ms="{{ last_run_epoch_ms or '' }}">
|
||||
|
||||
<div class="stat-row">
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
</div>
|
||||
|
||||
{% if can_create %}
|
||||
<!-- Modal: Neue Zugangsdaten -->
|
||||
<div class="modal-overlay" id="addCredentialModal">
|
||||
<div class="modal" style="max-width:1000px;">
|
||||
<form method="post" onsubmit="return validateCredentialForm(this, 'add');">
|
||||
@@ -122,7 +121,6 @@
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit %}
|
||||
<!-- Modal: Zugangsdaten bearbeiten -->
|
||||
<div class="modal-overlay" id="editCredentialModal">
|
||||
<div class="modal" style="max-width:380px;">
|
||||
<form method="post" onsubmit="return validateCredentialForm(this, 'edit');">
|
||||
|
||||
@@ -105,7 +105,6 @@
|
||||
</div>
|
||||
|
||||
{% if can_create %}
|
||||
<!-- Modal: Neues Gerät -->
|
||||
<div class="modal-overlay" id="deviceModal">
|
||||
<div class="modal" style="max-width:1000px;">
|
||||
<form method="post" onsubmit="return validateDeviceForm(this);">
|
||||
@@ -181,7 +180,6 @@
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit %}
|
||||
<!-- Modal: Bearbeiten -->
|
||||
<div class="modal-overlay" id="editDeviceModal">
|
||||
<div class="modal" style="max-width:1000px;">
|
||||
<form method="post" onsubmit="return validateDeviceForm(this);">
|
||||
@@ -250,7 +248,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Switch ändern -->
|
||||
<div class="modal-overlay" id="switchModal">
|
||||
<div class="modal" style="max-width:380px;">
|
||||
<form method="post">
|
||||
@@ -279,8 +276,6 @@
|
||||
{% endif %}
|
||||
|
||||
{% if can_create or can_edit %}
|
||||
<!-- Modal: SSH-Verbindungstest (echtes Terminal, zum Akzeptieren von Host-Keys
|
||||
und Prüfen der Zugangsdaten, bevor das Gerät gespeichert wird) -->
|
||||
<div class="modal-overlay" id="terminalModal">
|
||||
<div class="modal" style="max-width:720px;">
|
||||
<div class="modal-header">
|
||||
@@ -384,19 +379,12 @@ function toggleNewCredentialFields(select) {
|
||||
if (fields) fields.classList.toggle("hidden", select.value !== "new");
|
||||
}
|
||||
function resetCredentialChoice(id) {
|
||||
// Beim Öffnen sicherstellen, dass die "Neue Zugangsdaten"-Felder passend
|
||||
// zur aktuellen Auswahl ein-/ausgeblendet sind (relevant v.a. nach
|
||||
// vorherigem Umschalten auf "neu" ohne zu speichern).
|
||||
setTimeout(() => {
|
||||
const select = document.querySelector(`#${id === 'add' ? 'deviceModal' : 'editDeviceModal'} .credential-select`);
|
||||
if (select) toggleNewCredentialFields(select);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SSH-Verbindungstest (Web-Terminal via /ws/ssh_terminal) — identisch zum
|
||||
// Identisches Muster wie in switches.html.
|
||||
// -------------------------------------------------------------------------
|
||||
let term = null, fitAddon = null, termSocket = null, activePasswordInput = null;
|
||||
|
||||
function ensureTerminal() {
|
||||
|
||||
@@ -5,23 +5,6 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
{# Rendert einen Bereich (Geräte/Logs/Einstellungen) als kompakte, schmale
|
||||
Spalte: eigene Kopfzeile mit Bereichsname + einem Kästchen (der
|
||||
Kill-Switch, "Bereich sichtbar"), darunter eine kleine Tabelle mit den
|
||||
Unterpunkten (Clients, Switche, ...) als Zeilen und nur den für diesen
|
||||
Bereich tatsächlich genutzten Rechten (group_row_types[group_key]) als
|
||||
Spalten — Logs/Einstellungen zeigen z.B. nur R/W/E, nur Geräte zeigt
|
||||
zusätzlich D (PoE-Neustart über Dashboard, ausschließlich bei Clients).
|
||||
"Ändern" (E) deckt Löschen überall mit ab — es gibt nirgends ein eigenes
|
||||
Löschen-Recht. Zellen ohne passendes Recht für die jeweilige Zeile (z.B.
|
||||
"D" bei Switche/Zugangsdaten) werden als ausgegraute, nicht anklickbare
|
||||
Checkbox dargestellt statt zu fehlen — dadurch bleiben alle Spalten
|
||||
bündig untereinander. readonly=true zeigt nur den aktuellen Stand
|
||||
(Admin/Systemgruppen bzw. fehlendes groups.edit). Die JS-Funktion
|
||||
applyPermissionGating() sperrt Kind-Rechte, solange das Bereich-Lesen
|
||||
(Kill-Switch) nicht gesetzt ist. Mehrere Bereiche stehen per Flexbox
|
||||
nebeneinander (siehe .permission-groups-row), damit die ganze
|
||||
Rechteübersicht einer Gruppe in einer kompakten Zeile Platz hat. #}
|
||||
{% macro permission_table(group, group_key, checked_keys, readonly) %}
|
||||
{% set row_types = group_row_types[group_key] %}
|
||||
<div class="permission-group-col">
|
||||
@@ -70,10 +53,6 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# compact=true (Gruppen-Dropdown auf der Hauptseite) verkleinert Zellen-
|
||||
Padding/Abstände gegenüber dem "Neue Gruppe"-Modal (compact=false, bleibt
|
||||
unverändert) — gleicher Aufbau, nur enger, da im Dropdown mehr Breite
|
||||
zur Verfügung steht als im schmalen Modal und es sonst auseinandergezogen wirkt. #}
|
||||
{% macro permission_tree(checked_keys, readonly, compact=false) %}
|
||||
<div class="permission-groups-row{{ ' permission-groups-row--compact' if compact }}">
|
||||
{% for group_key, group in permission_catalog.items() %}
|
||||
@@ -119,9 +98,6 @@
|
||||
<th style="width:1%;">Aktionen</th>
|
||||
</tr></thead>
|
||||
|
||||
<!-- Virtuelle "Admin"-Gruppe: Rechte sind fix (alles), Mitgliedschaft
|
||||
wird direkt über is_admin gesteuert. Bleibt beim Sortieren immer
|
||||
oben (eigenes <tbody data-sort-pinned>). -->
|
||||
<tbody data-sort-pinned>
|
||||
<tr>
|
||||
<td class="cell-name">Admin <span class="pill admin">Systemrolle</span></td>
|
||||
@@ -147,8 +123,6 @@
|
||||
|
||||
{% for g in groups %}
|
||||
{% set can_edit_this = current_user.has_permission('groups.edit') and not g.is_system %}
|
||||
<!-- Jede Gruppe (Haupt- + Detail-Zeile) in einem eigenen <tbody>, damit
|
||||
die Sortierung beide Zeilen gemeinsam verschiebt. -->
|
||||
<tbody data-sort-name="{{ g.name|lower }}" data-sort-members="{{ g.member_names|length }}">
|
||||
<tr>
|
||||
<td class="cell-name">
|
||||
@@ -236,7 +210,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Admin-Mitglieder verwalten -->
|
||||
<div class="modal-overlay" id="adminMembersModal">
|
||||
<div class="modal" style="max-width:380px;">
|
||||
<form method="post">
|
||||
@@ -264,7 +237,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modals: Mitglieder pro Gruppe verwalten -->
|
||||
{% for g in groups %}
|
||||
<div class="modal-overlay" id="membersModal{{ loop.index }}">
|
||||
<div class="modal" style="max-width:380px;">
|
||||
@@ -298,9 +270,6 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Modal: Neue Gruppe — Rechte direkt beim Anlegen setzbar, Mitglieder
|
||||
werden danach über die Gruppentabelle zugeordnet (die Gruppe muss
|
||||
dafür erst existieren). -->
|
||||
<div class="modal-overlay" id="addGroupModal">
|
||||
<div class="modal" style="max-width:1000px;">
|
||||
<form method="post">
|
||||
@@ -329,9 +298,6 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Filtert ganze <tbody>-Blöcke (Haupt- + Detail-Zeile je Gruppe gemeinsam,
|
||||
// analog zur Sortierung) statt einzelner <tr> -- die gepinnte Admin-Gruppe
|
||||
// (data-sort-pinned) bleibt dabei immer sichtbar, unabhängig vom Suchbegriff.
|
||||
function filterGroupsTable() {
|
||||
const q = document.getElementById("groupSearch").value.trim().toLowerCase();
|
||||
document.querySelectorAll("#groupsTable tbody").forEach(tbody => {
|
||||
@@ -340,9 +306,6 @@ function filterGroupsTable() {
|
||||
});
|
||||
}
|
||||
|
||||
// Freischalten der Systemgruppe "Benutzer": zeigt statt der Nur-Lese-Ansicht
|
||||
// das editierbare Formular (inkl. unlock_system_group=1) -- serverseitig
|
||||
// erneut geprüft (admin + Flag), das hier ist nur die UI-Bestätigung.
|
||||
function unlockSystemGroup(id) {
|
||||
window.confirmAction(
|
||||
"Rechte der Standardgruppe „Benutzer“ wirklich bearbeiten? Diese Gruppe ist der Login-Fallback für neue Benutzer (auch neue AD/LDAP-Konten) — zu restriktive Rechte können deren Erst-Login einschränken.",
|
||||
@@ -362,12 +325,6 @@ function toggleDetail(id) {
|
||||
if (chev) chev.style.transform = row.classList.contains("hidden") ? "" : "rotate(180deg)";
|
||||
}
|
||||
|
||||
// Kind-Rechte einer Kategorie sind erst vergebbar, wenn das übergeordnete
|
||||
// "Bereich anzeigen"-Recht (Kill-Switch, erste Spalte/L-Zeile) gesetzt ist —
|
||||
// spiegelt serverseitig User.has_permission() (PERMISSION_PARENT_GROUP).
|
||||
// Bearbeitbare Tabellen bekommen einen Live-Listener, Nur-Lese-Tabellen
|
||||
// (Admin/Systemgruppen) bleiben unangetastet, deren Checkboxen sind ohnehin
|
||||
// alle disabled.
|
||||
function applyPermissionGating() {
|
||||
document.querySelectorAll(".permission-group-col").forEach(function (area) {
|
||||
const toggle = area.querySelector(".permission-area-toggle-cb");
|
||||
@@ -379,9 +336,6 @@ function applyPermissionGating() {
|
||||
cb.disabled = !toggle.checked;
|
||||
if (!toggle.checked) cb.checked = false;
|
||||
});
|
||||
// Zusätzlich zum disabled-Attribut (das der Browser nur dezent
|
||||
// abblendet) sichtbar ausgrauen, wie gefordert — sonst fällt
|
||||
// "Bereich gesperrt" auf den ersten Blick kaum auf.
|
||||
if (tbody) tbody.classList.toggle("permission-locked", !toggle.checked);
|
||||
};
|
||||
toggle.addEventListener("change", sync);
|
||||
@@ -389,11 +343,6 @@ function applyPermissionGating() {
|
||||
});
|
||||
}
|
||||
|
||||
// R (erste Spalte/"Lesen") ist je Zeile Voraussetzung für W/E/D/X -- ein
|
||||
// Recht ohne R wäre sonst über die UI nicht erreichbar (z.B. Nav-Link/Seite
|
||||
// bleibt unsichtbar, obwohl das Kind-Recht technisch gesetzt ist). Abwahl
|
||||
// von R nimmt deshalb automatisch auch die anderen Spalten dieser Zeile mit,
|
||||
// Anwahl von W/E/D/X wählt automatisch R mit an.
|
||||
function applyRowViewPrerequisite() {
|
||||
document.querySelectorAll(".permission-table tbody tr").forEach(function (tr) {
|
||||
const boxes = Array.from(tr.querySelectorAll(".permission-child-cb"));
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<!-- Device detail modal -->
|
||||
<div class="modal-overlay" id="deviceModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
@@ -50,17 +49,12 @@
|
||||
Neustarten
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Schließen bewusst als letztes Element im DOM: modal-footer nutzt
|
||||
justify-content:flex-end, damit landet der zuletzt gerenderte
|
||||
Button ganz rechts — Schließen soll unabhängig davon, welche
|
||||
Aktions-Buttons gerade sichtbar sind, immer rechts stehen. -->
|
||||
<button type="button" class="btn btn-secondary" data-close-modal>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.has_permission('devices.restart') %}
|
||||
<!-- Restart confirm modal -->
|
||||
<div class="modal-overlay" id="restartModal">
|
||||
<div class="modal" style="max-width:400px;">
|
||||
<div class="modal-header">
|
||||
@@ -90,21 +84,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const activateButton = document.getElementById("activateButton");
|
||||
const searchInput = document.getElementById("tileSearch");
|
||||
|
||||
// Status-Filter über die Stat-Kacheln (Online/Offline/Deaktiviert/
|
||||
// Gesamt) — null = kein Filter aktiv ("Gesamt"). Zusammen mit dem
|
||||
// Suchfilter kombiniert in applyFilters(), damit beide gleichzeitig
|
||||
// greifen können (z.B. "offline" + Namenssuche).
|
||||
let activeStatusFilter = null;
|
||||
// Welche Abschnitte (per data-status) manuell eingeklappt wurden — per
|
||||
// Klick auf den Abschnittstitel, unabhängig vom Status-Filter. Bleibt
|
||||
// über Live-Updates hinweg erhalten (siehe applyCollapseState()).
|
||||
const collapsedSections = new Set();
|
||||
|
||||
// Kombinierter Filter über alle Abschnitte hinweg: ein Abschnitt wird
|
||||
// komplett ausgeblendet, wenn er nicht zum Status-Filter passt ODER
|
||||
// keine seiner Kacheln mehr zur Namenssuche passt. Wird nach jedem
|
||||
// Live-Update erneut angewendet, damit aktive Filter erhalten bleiben
|
||||
// (die alten DOM-Knoten werden beim Austausch verworfen).
|
||||
function applyFilters() {
|
||||
const q = (searchInput ? searchInput.value : "").trim().toLowerCase();
|
||||
let anyVisible = false;
|
||||
@@ -121,9 +103,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
const noResults = document.getElementById("noSearchResults");
|
||||
if (noResults) noResults.classList.toggle("hidden", anyVisible || (q === "" && !activeStatusFilter));
|
||||
// Aktive Stat-Kachel synchron halten — läuft hier statt nur im
|
||||
// Klick-Handler, damit der Zustand auch nach einem Live-Update
|
||||
// (frische, neu eingefügte Stat-Kacheln) sofort wieder stimmt.
|
||||
document.querySelectorAll(".stat-card[data-status-filter]").forEach(c => {
|
||||
const isTotal = c.dataset.statusFilter === "";
|
||||
const active = activeStatusFilter === null ? isTotal : c.dataset.statusFilter === activeStatusFilter;
|
||||
@@ -133,19 +112,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
if (searchInput) searchInput.addEventListener("input", applyFilters);
|
||||
|
||||
// Ein-/Ausgeklappt-Zustand je Abschnitt anhand von collapsedSections
|
||||
// anwenden — separat von der Sichtbarkeit oben: "hidden" blendet den
|
||||
// ganzen Abschnitt (inkl. Titel) aus, "collapsed" versteckt nur das
|
||||
// Kachel-Raster darunter, der Titel bleibt klickbar sichtbar.
|
||||
function applyCollapseState() {
|
||||
document.querySelectorAll(".dash-section").forEach(section => {
|
||||
section.classList.toggle("collapsed", collapsedSections.has(section.dataset.status));
|
||||
});
|
||||
}
|
||||
|
||||
// Stat-Kacheln (Online/Offline/Deaktiviert/Gesamt): Klick filtert das
|
||||
// Dashboard auf genau diesen Status; erneuter Klick auf die bereits
|
||||
// aktive Kachel (oder auf "Gesamt") hebt den Filter wieder auf.
|
||||
function bindStatCards() {
|
||||
document.querySelectorAll(".stat-card[data-status-filter]").forEach(card => {
|
||||
card.addEventListener("click", function () {
|
||||
@@ -156,8 +128,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Abschnittstitel (z.B. "Offline"): Klick klappt nur diesen Abschnitt
|
||||
// ein/aus, unabhängig vom Status-Filter oben.
|
||||
function bindSectionToggles() {
|
||||
document.querySelectorAll(".dash-section-title").forEach(title => {
|
||||
title.addEventListener("click", function () {
|
||||
@@ -168,9 +138,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Klick-Verhalten der Kacheln (Detail-Modal, Neustart/Aktivieren) — wird
|
||||
// nach jedem Live-Update erneut auf die frisch eingefügten Kacheln
|
||||
// angewendet, da deren alte DOM-Knoten beim Austausch verworfen werden.
|
||||
function bindDeviceCards() {
|
||||
if (!isAuthenticated) return;
|
||||
document.querySelectorAll(".device-card").forEach(card => {
|
||||
@@ -198,10 +165,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const pill = this.querySelector(".pill");
|
||||
document.getElementById("deviceStatus").innerText = pill ? pill.innerText.trim() : "-";
|
||||
if (restartButton) {
|
||||
// Ohne zugewiesenen Switch + Port kann poe.sh keinen PoE-Reset
|
||||
// auslösen (siehe restart_device()/poe.sh) — der Button wird für
|
||||
// diese Geräte komplett ausgeblendet statt nur ausgegraut, damit
|
||||
// kein Neustart vorgegaukelt wird, der tatsächlich nichts bewirkt.
|
||||
const hasSwitchAndPort = this.dataset.switch !== "-" && this.dataset.port !== "-";
|
||||
restartButton.style.display = (isActive && hasSwitchAndPort) ? "" : "none";
|
||||
restartButton.disabled = false;
|
||||
@@ -217,10 +180,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
bindSectionToggles();
|
||||
applyFilters();
|
||||
|
||||
// Live-Update der Dashboard-Kacheln ohne vollen Seiten-Reload: tauscht
|
||||
// nur den #dashboard-tiles-Container gegen das AJAX-Partial aus (siehe
|
||||
// /dashboard/tiles) und hält dabei Suchfilter, Scroll-Position und
|
||||
// Sidebar-Zustand unangetastet. Läuft für an- und abgemeldete Ansicht.
|
||||
let refreshTimer = null;
|
||||
|
||||
function scheduleNextRefresh(overrideMs) {
|
||||
@@ -230,7 +189,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const intervalMs = pill ? parseInt(pill.dataset.intervalMs, 10) : NaN;
|
||||
if (!intervalMs) return;
|
||||
const lastRunMs = parseInt(pill.dataset.lastRunMs, 10);
|
||||
// + kleiner Puffer, bis poe.sh die neue Zeile tatsächlich geschrieben hat.
|
||||
let waitMs = intervalMs + 1500;
|
||||
if (!isNaN(lastRunMs) && lastRunMs > 0) {
|
||||
const elapsed = Date.now() - lastRunMs;
|
||||
@@ -250,9 +208,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const fresh = temp.querySelector("#dashboard-tiles");
|
||||
if (!fresh) return;
|
||||
container.replaceWith(fresh);
|
||||
// Globalen Topbar-Timer mit dem tatsächlichen Zeitpunkt
|
||||
// dieses Durchlaufs synchronisieren (initCheckTimer() in
|
||||
// app.js liest das Attribut bei jedem Tick neu ein).
|
||||
const pill = document.getElementById("global-timer-pill");
|
||||
if (pill) pill.dataset.lastRunMs = fresh.dataset.lastRunMs || "";
|
||||
bindDeviceCards();
|
||||
|
||||
@@ -32,8 +32,6 @@ function colorizeLine(line) {
|
||||
if (line.includes(" ist erreichbar!")) cls = "online";
|
||||
else if (line.includes(" ist nicht erreichbar!")) cls = "offline";
|
||||
else if (line.startsWith("----")) cls = "sep";
|
||||
// Neustart-bezogene Zeilen (manueller Neustart, automatischer PoE-Restart
|
||||
// bei Ausfall) einheitlich orange markieren.
|
||||
else if (line.toLowerCase().includes("manuell") || line.includes("PoE")) cls = "restart";
|
||||
const span = document.createElement("span");
|
||||
span.className = "log-line" + (cls ? " " + cls : "");
|
||||
@@ -54,8 +52,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (i < lines.length - 1) box.appendChild(document.createElement("br"));
|
||||
});
|
||||
box.scrollTop = box.scrollHeight;
|
||||
// Dateiname mit anzeigen — ändert sich nach einem Service-Neustart
|
||||
// (neues Logfile, z.B. durch "Jetzt prüfen") ohne Seiten-Reload.
|
||||
if (logName) {
|
||||
document.querySelectorAll("[data-log-name]").forEach((el) => { el.textContent = logName; });
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@
|
||||
<script>
|
||||
function colorizeLine(line) {
|
||||
let cls = "";
|
||||
// Kea-Zeilenformat: "<ts> <SEVERITY> [logger/pid.tid] MESSAGE_ID text".
|
||||
// Grobe Einfärbung nach Schweregrad, ohne die Logik des Live-Logs
|
||||
// (online/offline) hier künstlich nachzubilden.
|
||||
if (/\s(FATAL|ERROR)\s/.test(line)) cls = "offline";
|
||||
else if (/\sWARN\s/.test(line)) cls = "restart";
|
||||
const span = document.createElement("span");
|
||||
|
||||
@@ -68,15 +68,6 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
{% if can_run %}
|
||||
<!-- Bewusst KEIN <form> hier: das säße dann innerhalb des
|
||||
äußeren #updateForm (verschachtelte <form>-Elemente sind
|
||||
ungültiges HTML) -- der Browser hätte das äußere Formular
|
||||
an dieser Stelle vorzeitig geschlossen bzw. dessen Inhalt
|
||||
zerrissen. "Update starten" hat dadurch live IMMER
|
||||
zusätzlich das erste/oberste Gerät mitgeschickt, auch ganz
|
||||
ohne angehakte Checkbox. Das eigentliche <form> für den
|
||||
Neustart steht daher unten AUSSERHALB von #updateForm; der
|
||||
Button hier referenziert es nur über "form=". -->
|
||||
<button type="submit" form="reboot-{{ d['mac'] }}" class="btn btn-sm btn-secondary">Neustart</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -104,10 +95,6 @@
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<!-- Ein eigenständiges, NICHT verschachteltes <form> je Gerät für den
|
||||
einzelnen "Neustart"-Button (siehe Kommentar oben in der Tabelle) --
|
||||
jedes referenziert nur genau eine MAC, unabhängig vom Checkbox-Zustand
|
||||
im großen #updateForm. -->
|
||||
{% if can_run %}
|
||||
{% for d in devices %}
|
||||
<form method="post" action="{{ url_for('maintenance_reboot') }}" id="reboot-{{ d['mac'] }}" data-confirm="{{ d['name'] }} ({{ d['ip'] }}) jetzt per SSH neu starten?">
|
||||
@@ -121,14 +108,6 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Checkboxen beim Laden IMMER zurücksetzen: Browser stellen den
|
||||
// Checked-Zustand von Formularen nach einem Reload/Redirect (z.B. genau der
|
||||
// Redirect nach "Update starten") gerne aus dem eigenen Verlauf wieder her,
|
||||
// unabhängig davon, dass das frisch gerenderte HTML gar kein "checked"
|
||||
// enthält -- ein einmal angehaktes Gerät (z.B. beim ersten Ausprobieren)
|
||||
// blieb dadurch bei jedem weiteren Aufruf der Seite unsichtbar mitausgewählt
|
||||
// und lief bei "Update starten" immer zusätzlich mit, ganz gleich was man
|
||||
// gerade neu ankreuzt (live genau so reproduziert).
|
||||
document.querySelectorAll(".maint-check").forEach(function (cb) { cb.checked = false; });
|
||||
const selectAllCb = document.getElementById("selectAll");
|
||||
if (selectAllCb) selectAllCb.checked = false;
|
||||
@@ -142,9 +121,6 @@ document.getElementById("selectAll")?.addEventListener("change", function () {
|
||||
function filterTable(inputId, tableId) {
|
||||
const q = document.getElementById(inputId).value.trim().toLowerCase();
|
||||
document.querySelectorAll(`#${tableId} tbody tr`).forEach(row => {
|
||||
// Ausgabe-Zeile bleibt ausschließlich über toggleJobOutput() gesteuert,
|
||||
// sonst könnte ein zufälliger Text-Treffer darin sie unabhängig vom
|
||||
// dazugehörigen Geräte-Namen ein-/ausblenden.
|
||||
if (row.classList.contains("empty-row") || row.classList.contains("job-output-row")) return;
|
||||
row.style.display = row.innerText.toLowerCase().includes(q) ? "" : "none";
|
||||
});
|
||||
@@ -161,11 +137,6 @@ function toggleJobOutput(mac) {
|
||||
}
|
||||
}
|
||||
|
||||
// "Klebt" am unteren Rand, solange man nicht selbst nach oben gescrollt hat
|
||||
// -- reine Update-Ausgabe kann bei vielen Paketen sehr lang werden, ohne
|
||||
// das würde man sie sonst manuell nachziehen müssen, um live mitzulesen.
|
||||
// Ein kleiner Schwellwert (20px) toleriert, dass "ganz unten" durch
|
||||
// Rundungsfehler beim Scrollen selten exakt 0 ist.
|
||||
function isNearBottom(el) {
|
||||
return el.scrollHeight - el.scrollTop - el.clientHeight < 20;
|
||||
}
|
||||
|
||||
@@ -308,11 +308,6 @@ function moveNavItem(btn, dir) {
|
||||
else li.parentNode.insertBefore(target, li);
|
||||
}
|
||||
|
||||
// Lädt beim Umschalten des Interfaces (Netzwerkeinstellungen) dessen
|
||||
// tatsächlichen Ist-Zustand nach -- ohne das würden die Felder weiter die
|
||||
// Werte des zuvor angezeigten Interfaces zeigen, obwohl gerade ein anderes
|
||||
// ausgewählt wurde (bei mehreren Interfaces leicht zu Verwechslungen
|
||||
// geführt, siehe /settings/network_state).
|
||||
function loadNetworkState(iface) {
|
||||
fetch("{{ url_for('settings_network_state') }}?interface=" + encodeURIComponent(iface))
|
||||
.then(r => r.json())
|
||||
|
||||
@@ -418,10 +418,6 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modals außerhalb der Tabelle: ein <div> direkt in <tbody> ist
|
||||
ungültiges HTML und wird vom Browser aus der Tabelle heraus
|
||||
"foster-parented" — dabei kann die Eltern-Kind-Beziehung zwischen
|
||||
Formular und Feldern zerrissen werden. -->
|
||||
{% if option_defs and can_edit %}
|
||||
{% for r in reservations %}
|
||||
<div class="modal-overlay" id="deviceOptionsModal{{ loop.index }}">
|
||||
@@ -446,10 +442,6 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<!-- Neu ausgewählte Felder landen hier direkt unter dem Dropdown
|
||||
(siehe poeShowOptionField in app.js), statt irgendwo in der
|
||||
u.U. langen Liste unten sichtbar zu werden. Bereits gesetzte
|
||||
Overrides stehen von Anfang an hier drin. -->
|
||||
<div class="poe-added-options" style="margin:10px 0;">
|
||||
{% for d in option_defs %}
|
||||
{% set current = option_values.get(d.id, {}).get(r.mac, '') %}
|
||||
@@ -604,8 +596,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bearbeiten-Modals außerhalb der Karte/Tabelle (siehe Kommentar oben
|
||||
bei den Reservierungen — dieselbe HTML-Validität-Begründung). -->
|
||||
{% if can_edit %}
|
||||
{% for s in subnets %}
|
||||
<div class="modal-overlay" id="editSubnetModal{{ loop.index }}">
|
||||
|
||||
@@ -83,11 +83,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bearbeiten-Modals außerhalb der Tabelle: ein <div> direkt in <tbody>
|
||||
ist ungültiges HTML — Browser "foster-parenten" es dann aus der
|
||||
Tabelle heraus und zerreißen dabei mitunter die Eltern-Kind-Beziehung
|
||||
zwischen Formular und Buttons (this.closest('form') lieferte dadurch
|
||||
null, "Verbindung testen" öffnete kein Terminal mehr). -->
|
||||
{% if can_edit %}
|
||||
{% for s in switches %}
|
||||
<div class="modal-overlay" id="editSwitchModal{{ loop.index }}">
|
||||
@@ -151,7 +146,6 @@
|
||||
{% endif %}
|
||||
|
||||
{% if can_create %}
|
||||
<!-- Modal: Neuer Switch -->
|
||||
<div class="modal-overlay" id="addSwitchModal">
|
||||
<div class="modal" style="max-width:1000px;">
|
||||
<form method="post" onsubmit="return validateSwitchForm(this, 'add');">
|
||||
@@ -214,8 +208,6 @@
|
||||
{% endif %}
|
||||
|
||||
{% if can_create or can_edit %}
|
||||
<!-- Modal: SSH-Verbindungstest (echtes Terminal, zum Akzeptieren von Host-Keys
|
||||
und Prüfen der Zugangsdaten, bevor der Switch gespeichert wird) -->
|
||||
<div class="modal-overlay" id="terminalModal">
|
||||
<div class="modal" style="max-width:720px;">
|
||||
<div class="modal-header">
|
||||
@@ -252,26 +244,17 @@
|
||||
<script src="{{ url_for('static', filename='js/vendor/xterm.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/vendor/xterm-addon-fit.js') }}"></script>
|
||||
<script>
|
||||
// -------------------------------------------------------------------------
|
||||
// Zugangsdaten-Auswahl: "+ Neue Zugangsdaten anlegen" blendet die Felder ein
|
||||
// -------------------------------------------------------------------------
|
||||
function toggleNewCredentialFields(select) {
|
||||
const fields = select.closest(".modal-body").querySelector(".new-credential-fields");
|
||||
if (fields) fields.classList.toggle("hidden", select.value !== "new");
|
||||
}
|
||||
function resetCredentialChoice(id) {
|
||||
// Beim Öffnen sicherstellen, dass die "Neue Zugangsdaten"-Felder passend
|
||||
// zur aktuellen Auswahl ein-/ausgeblendet sind (relevant v.a. nach
|
||||
// vorherigem Umschalten auf "neu" ohne zu speichern).
|
||||
setTimeout(() => {
|
||||
const select = document.querySelector(`#${id === 'add' ? 'addSwitchModal' : 'editSwitchModal' + id.replace('edit','')} .credential-select`);
|
||||
if (select) toggleNewCredentialFields(select);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SSH-Verbindungstest (Web-Terminal via /ws/ssh_terminal)
|
||||
// -------------------------------------------------------------------------
|
||||
let term = null, fitAddon = null, termSocket = null, activePasswordInput = null;
|
||||
|
||||
function ensureTerminal() {
|
||||
@@ -347,10 +330,6 @@ function openTerminal(form) {
|
||||
};
|
||||
termSocket.onmessage = (event) => term.write(event.data);
|
||||
termSocket.onclose = () => setTermStatus("Getrennt", "offline");
|
||||
// Bewusst kein Toast bei "error": manche Browser melden beim Schließen
|
||||
// einer WebSocket-Verbindung ein error-Event, obwohl die Sitzung
|
||||
// inhaltlich erfolgreich war. Terminal-Inhalt + Status-Pill genügen als
|
||||
// Feedback — der Verbindungsstatus wird zuverlässig über onclose gepflegt.
|
||||
termSocket.onerror = () => setTermStatus("Fehler", "offline");
|
||||
}
|
||||
|
||||
@@ -376,7 +355,6 @@ if (termPasteBtn) {
|
||||
});
|
||||
}
|
||||
|
||||
// Verbindung auch beim Schließen per Klick auf Backdrop / Escape sauber trennen
|
||||
const terminalModalEl = document.getElementById("terminalModal");
|
||||
if (terminalModalEl) {
|
||||
terminalModalEl.addEventListener("click", (e) => {
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Neuer Benutzer -->
|
||||
<div class="modal-overlay" id="userModal">
|
||||
<div class="modal" style="max-width:1000px;">
|
||||
<form method="post" id="userForm">
|
||||
@@ -138,8 +137,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Benutzer bearbeiten (nur lokale Konten -- AD/LDAP-Konten lassen
|
||||
sich nur sperren/entsperren und einer Gruppe zuweisen, siehe Tabelle) -->
|
||||
<div class="modal-overlay" id="editModal">
|
||||
<div class="modal">
|
||||
<form method="post" id="editForm">
|
||||
@@ -165,8 +162,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Aus Active Directory hinzufügen (Vorab-Zuweisung vor dem ersten
|
||||
Login des AD-Benutzers) -->
|
||||
{% if ldap_enabled %}
|
||||
<div class="modal-overlay" id="ldapAddModal">
|
||||
<div class="modal">
|
||||
@@ -200,7 +195,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Modal: Gruppe zuweisen (inkl. Admin als Auswahl) -->
|
||||
<div class="modal-overlay" id="groupModal">
|
||||
<div class="modal" style="max-width:380px;">
|
||||
<form method="post" id="groupForm">
|
||||
|
||||
Reference in New Issue
Block a user