Hostname-Einstellung, DHCP-Range-Pflicht, Topbar-Ampel, Text-Straffung, Quelltext-Huerde
Hostname (Systemeinstellungen, neben Pruefintervall): - hostnamectl set-hostname ueber die App, Validierung (RFC-1123-artiges Label), kein Revert-Timer noetig (kappt die Erreichbarkeit nicht wie eine IP-Aenderung). Live getestet inkl. Validierung und Revert. DHCP: Range ist jetzt Pflicht, kein geratener Default mehr: - dhcp_range_start/end/dns starten leer (Platzhaltertext statt fake- echt aussehendem Default) - ein Zufalls-Range haette den Dienst sonst unbemerkt mit einer zum echten Netz nicht passenden Konfiguration starten lassen koennen. - write_dhcp_file und dhcp_service_action=enable_restart verweigern sich ohne eingetragene Range; _render_kea_config laesst "pools" ohne Range komplett weg statt einen kaputten Pool-String zu erzeugen. - Neues, separates dhcp_gateway-Feld (optional) fuer einen vom automatisch erkannten Gateway abweichenden Router fuer die Clients. Topbar-Ampel fuer den Kea-Dienst: - Gruener/roter Punkt neben dem Pruefintervall-Timer, nur sichtbar mit settings_dhcp.view (ein einzelner, kurzer systemctl-Aufruf pro Request, nicht die volle Status-Erkennung). Live verifiziert (rot wenn gestoppt, gruen wenn gestartet). Reservierungstabelle zeigt jetzt pro Client, welche eigene DHCP-Option greift (global oder Client-Override, mit Wert im Tooltip). Text-Straffung: die laengsten Hint-Texte und Code-Kommentare in Templates/app.py gekuerzt (u.a. groups.html, settings_dhcp.html, account.html, devices/switches/users/credentials.html, zwei grosse Migrations-/DHCP-Kommentarbloecke in app.py) - Kernaussagen erhalten, Redundanz entfernt. Quelltext-Huerde (KEINE echte Sicherheit, nur Abschreckung): Rechtsklick und DevTools-/Quelltext-Shortcuts per JS blockiert. Klar dokumentiert in Kommentar + README, dass der Browser HTML/CSS/JS immer vollstaendig ausliefert und das in Sekunden umgehbar ist - echte Absicherung bleiben ausschliesslich die serverseitigen Rechteprüfungen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+81
-51
@@ -17,7 +17,7 @@ from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from datetime import datetime
|
||||
import base64, ipaddress, shutil, sqlite3, glob, json, os, re, secrets, subprocess, threading, time, traceback
|
||||
import base64, ipaddress, shutil, socket, sqlite3, glob, json, os, re, secrets, subprocess, threading, time, traceback
|
||||
import paramiko
|
||||
|
||||
|
||||
@@ -623,24 +623,12 @@ def _ensure_schema():
|
||||
(cur.lastrowid, sw["hostname"]),
|
||||
)
|
||||
|
||||
# Migration (Einführung des granularen, an die Navbar gespiegelten
|
||||
# Rechtebaums): alte Permission-Keys auf die neuen übertragen, damit
|
||||
# bestehende Gruppen durch das Update keinen Zugriff verlieren.
|
||||
# - devices.toggle ist jetzt Teil von devices.edit (Ändern).
|
||||
# - Zugangsdaten hatten bisher keine eigenen Rechte, sondern hingen an
|
||||
# switches.* — bestehende Gruppen bekommen die entsprechenden
|
||||
# credentials.*-Rechte gespiegelt, künftig sind beide unabhängig.
|
||||
# - Jede Gruppe, die bereits ein Kind-Recht in einem Bereich hatte,
|
||||
# bekommt automatisch auch das neue übergeordnete "Bereich
|
||||
# sichtbar"-Recht (devices_group.view/logs_group.view/
|
||||
# settings_group.view), da Kind-Rechte jetzt ohne dieses Recht wirkungslos
|
||||
# wären (siehe User.has_permission).
|
||||
# WICHTIG: Läuft nur EINMALIG (Guard über settings), nicht bei jedem
|
||||
# Start — sonst würde das übergeordnete "Bereich sichtbar"-Recht bei
|
||||
# jedem Neustart automatisch wieder gesetzt, sobald irgendein Kind-Recht
|
||||
# noch aktiv ist. Das würde den eigentlichen Zweck des Kill-Switches
|
||||
# zunichtemachen (ein Admin könnte den ganzen Bereich nie dauerhaft
|
||||
# sperren, ohne zuvor jedes einzelne Kind-Recht zu entfernen).
|
||||
# Migration (granularer Rechtebaum): devices.toggle -> devices.edit,
|
||||
# switches.* -> gespiegelte credentials.*, sowie Backfill des neuen
|
||||
# "Bereich sichtbar"-Rechts für Gruppen mit bereits gesetzten Kind-Rechten.
|
||||
# WICHTIG: nur EINMALIG (Guard über settings) — sonst würde das
|
||||
# Bereichs-Recht bei jedem Neustart automatisch neu gesetzt und den
|
||||
# Kill-Switch (siehe User.has_permission) wirkungslos machen.
|
||||
_migration_key = "_migrated_permission_hierarchy_v1"
|
||||
if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key,)).fetchone():
|
||||
toggle_groups = conn.execute(
|
||||
@@ -1077,6 +1065,22 @@ def inject_check_timer():
|
||||
}
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_dhcp_topbar_status():
|
||||
"""Ampel-Punkt für den Kea-Dienst neben dem Prüfintervall-Timer — nur
|
||||
für Benutzer mit mindestens Leserecht auf DHCP, und nur ein einzelner,
|
||||
kurzer systemctl-Aufruf (nicht die volle Status-Erkennung von
|
||||
_detect_dhcp_status), damit das nicht jede Seite spürbar verlangsamt."""
|
||||
if not current_user.is_authenticated or not current_user.has_permission("settings_dhcp.view"):
|
||||
return {"dhcp_topbar_active": None}
|
||||
try:
|
||||
result = subprocess.run(["systemctl", "is-active", DHCP_SERVICE], capture_output=True, text=True, timeout=2)
|
||||
active = result.stdout.strip() == "active"
|
||||
except Exception:
|
||||
active = None
|
||||
return {"dhcp_topbar_active": active}
|
||||
|
||||
|
||||
def get_device_status(devices):
|
||||
"""
|
||||
Ermittelt aus dem aktuellsten Logfile in einem Durchlauf je Gerät:
|
||||
@@ -1229,21 +1233,12 @@ def run_check_now():
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# System-Netzwerkeinstellungen (IP/DNS/DHCP DIESES Hosts — nicht zu
|
||||
# verwechseln mit dem Kea-DHCP-*Server* für Client-Geräte weiter unten).
|
||||
#
|
||||
# Höchste Vorsicht geboten: eine falsche Änderung hier kann die Erreichbarkeit
|
||||
# dieses Hosts (SSH/Web-UI) komplett kappen. Deshalb:
|
||||
# - Erkennung des tatsächlich aktiven Netzwerk-Backends (NetworkManager
|
||||
# oder dhcpcd) statt blind eine Methode anzunehmen; ist keins von beiden
|
||||
# aktiv (z.B. in dieser WSL-Testumgebung), bleibt die Seite bewusst rein
|
||||
# lesend statt zu raten.
|
||||
# - Vor jeder Änderung wird die vorherige Konfiguration gesichert.
|
||||
# - Nach dem Anwenden läuft ein Sicherheits-Timer im Hintergrund: wird die
|
||||
# neue Konfiguration nicht innerhalb von NETWORK_REVERT_SECONDS explizit
|
||||
# bestätigt (das ist nur möglich, wenn die Web-UI über die NEUE
|
||||
# Konfiguration noch erreichbar ist), wird automatisch die gesicherte
|
||||
# Konfiguration wiederhergestellt — analog zu "netplan try".
|
||||
# System-Netzwerkeinstellungen (IP/DNS/DHCP DIESES Hosts — nicht der
|
||||
# Kea-DHCP-*Server* für Client-Geräte weiter unten). Eine falsche Änderung
|
||||
# kann die Erreichbarkeit kappen, daher: Backend-Erkennung statt Annahme
|
||||
# (unbekannt -> rein lesend), Backup vor jeder Änderung, und ein
|
||||
# Sicherheits-Timer (analog "netplan try") der ohne explizite Bestätigung
|
||||
# innerhalb von NETWORK_REVERT_SECONDS automatisch zurückrollt.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NETWORK_REVERT_SECONDS = 45
|
||||
@@ -1470,6 +1465,18 @@ def settings():
|
||||
else:
|
||||
flash(f"Anwenden fehlgeschlagen: {out}", "danger")
|
||||
|
||||
elif "hostname" in request.form:
|
||||
new_hostname = request.form.get("hostname", "").strip()
|
||||
if not re.match(r"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", new_hostname):
|
||||
flash("Ungültiger Hostname (nur Buchstaben, Ziffern und Bindestrich, max. 63 Zeichen).", "danger")
|
||||
else:
|
||||
ok, out = _dhcp_run_privileged(["hostnamectl", "set-hostname", new_hostname], timeout=10)
|
||||
if ok:
|
||||
log_action("settings.update", "Hostname", new_hostname)
|
||||
flash(f"Hostname auf „{new_hostname}“ gesetzt.", "success")
|
||||
else:
|
||||
flash(f"Hostname konnte nicht gesetzt werden: {out}", "danger")
|
||||
|
||||
elif "confirm_network" in request.form:
|
||||
token = request.form.get("confirm_network")
|
||||
entry = _pending_network_revert.pop(token, None)
|
||||
@@ -1486,9 +1493,14 @@ def settings():
|
||||
net_interface = get_setting("net_interface") or (_list_network_interfaces() or [None])[0]
|
||||
net_state = _read_network_state(net_interface, net_backend) if net_interface else None
|
||||
pending_token = next(iter(_pending_network_revert), None)
|
||||
try:
|
||||
current_hostname = socket.gethostname()
|
||||
except Exception:
|
||||
current_hostname = None
|
||||
return render_template(
|
||||
"settings.html",
|
||||
interval=interval,
|
||||
current_hostname=current_hostname,
|
||||
net_backend=net_backend,
|
||||
net_interfaces=_list_network_interfaces(),
|
||||
net_interface=net_interface,
|
||||
@@ -1510,17 +1522,11 @@ def settings_import_export():
|
||||
# ---------------------------------------------------------------------------
|
||||
# DHCP-Reservierungen (Kea DHCP)
|
||||
#
|
||||
# Backend bewusst Kea (kea-dhcp4-server), nicht das ältere isc-dhcp-server:
|
||||
# Kea wird von ISC selbst aktiv weiterentwickelt (isc-dhcp-server ist seit
|
||||
# 2022 EOL) und bildet "globaler Wert, pro Client überschreibbar" nativ ab
|
||||
# (Reservation-Options überschreiben in Kea automatisch gleichnamige
|
||||
# globale/Subnet-Options) — genau das von den Custom-Options gefordert.
|
||||
#
|
||||
# Installation & Dienststeuerung sind eigene, einzeln bestätigte Aktionen
|
||||
# (apt-get install / systemctl enable+restart) statt automatisch an
|
||||
# "Konfiguration speichern" gekoppelt — ein unerwartet aktiver oder
|
||||
# fehlkonfigurierter DHCP-Server kann ein ganzes Netzsegment stören, das
|
||||
# soll immer ein bewusster, separater Klick bleiben.
|
||||
# Backend bewusst Kea statt isc-dhcp-server (seit 2022 EOL) — Kea bildet
|
||||
# "globaler Wert, pro Client überschreibbar" nativ über Reservation-Options
|
||||
# ab. Installation & Dienststeuerung sind eigene, einzeln bestätigte
|
||||
# Aktionen statt an "Konfiguration speichern" gekoppelt, da ein
|
||||
# unerwarteter DHCP-Server ein Netzsegment stören kann.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DHCP_PACKAGE = "kea-dhcp4-server"
|
||||
@@ -1528,15 +1534,26 @@ DHCP_SERVICE = "kea-dhcp4-server"
|
||||
|
||||
DHCP_DEFAULTS = {
|
||||
"dhcp_interface": "eth0",
|
||||
"dhcp_range_start": "192.168.1.100",
|
||||
"dhcp_range_end": "192.168.1.200",
|
||||
"dhcp_dns": "192.168.1.1",
|
||||
# Bewusst KEINE vorausgefüllten Range-/DNS-Defaults, die wie eine echte
|
||||
# Konfiguration aussehen könnten, aber zufällig nicht zum echten Netz
|
||||
# passen — leer + Platzhaltertext im Formular ist ehrlicher, als einen
|
||||
# Dienst unbemerkt mit falschen Werten starten zu lassen.
|
||||
"dhcp_range_start": "",
|
||||
"dhcp_range_end": "",
|
||||
"dhcp_dns": "",
|
||||
# Optionaler Override — leer bedeutet "automatisch erkanntes Gateway
|
||||
# dieses Hosts verwenden" (siehe _render_kea_config).
|
||||
"dhcp_gateway": "",
|
||||
"dhcp_domain": "local",
|
||||
"dhcp_lease_default": "600",
|
||||
"dhcp_lease_max": "7200",
|
||||
"dhcp_output_path": "/etc/kea/kea-dhcp4.conf",
|
||||
}
|
||||
|
||||
|
||||
def _dhcp_config_is_complete(cfg):
|
||||
return bool(cfg.get("dhcp_range_start") and cfg.get("dhcp_range_end"))
|
||||
|
||||
# Auswahl an Kea-Datentypen für eigene Options — bewusst eine kompatible
|
||||
# Teilmenge statt der vollen Liste, orientiert an den in der Praxis
|
||||
# gebrauchten Fällen (Text, IP, Zahl, Wahrheitswert, Rohdaten/Hex).
|
||||
@@ -1718,7 +1735,10 @@ def _render_kea_config(cfg, net_info, reservations, option_defs, option_values):
|
||||
Options für genau diesen Client, ohne dass wir das selbst
|
||||
zusammenrechnen müssen."""
|
||||
dns_servers = ", ".join(s.strip() for s in cfg["dhcp_dns"].split(",") if s.strip())
|
||||
router = net_info.get("gateway") or (cfg["dhcp_dns"].split(",")[0].strip() if cfg["dhcp_dns"] else "")
|
||||
# Gateway: expliziter Override hat Vorrang, sonst automatisch erkanntes
|
||||
# Gateway dieses Hosts — für den Fall, dass Clients einen anderen Router
|
||||
# bekommen sollen als den, über den dieser Host selbst geht.
|
||||
router = cfg.get("dhcp_gateway", "").strip() or net_info.get("gateway") or ""
|
||||
|
||||
option_def_list = [
|
||||
{"name": d["name"], "code": d["code"], "type": d["type"], "space": "dhcp4"}
|
||||
@@ -1765,7 +1785,11 @@ def _render_kea_config(cfg, net_info, reservations, option_defs, option_values):
|
||||
{
|
||||
"id": 1,
|
||||
"subnet": f"{net_info['network'].network_address}/{net_info['network'].prefixlen}",
|
||||
"pools": [{"pool": f"{cfg['dhcp_range_start']} - {cfg['dhcp_range_end']}"}],
|
||||
# Kein Pool ohne eingetragene Range — ein leerer/falscher
|
||||
# Pool-String wäre eine Kea-Konfiguration, die entweder
|
||||
# gar nicht startet oder (schlimmer) mit einer geratenen
|
||||
# Range startet, die nicht zum echten Netz passt.
|
||||
**({"pools": [{"pool": f"{cfg['dhcp_range_start']} - {cfg['dhcp_range_end']}"}]} if _dhcp_config_is_complete(cfg) else {}),
|
||||
"reservations": kea_reservations,
|
||||
}
|
||||
] if net_info.get("ok") else [],
|
||||
@@ -1882,6 +1906,10 @@ def settings_dhcp():
|
||||
|
||||
elif "write_dhcp_file" in request.form:
|
||||
cfg = _load_dhcp_settings()
|
||||
if not _dhcp_config_is_complete(cfg):
|
||||
flash("Range Start/Ende müssen eingetragen sein, bevor eine Konfiguration geschrieben wird.", "danger")
|
||||
conn.close()
|
||||
return redirect(url_for("settings_dhcp"))
|
||||
net_info = _detect_interface_network(cfg["dhcp_interface"])
|
||||
if not net_info["ok"]:
|
||||
flash(f"Netzwerk-Erkennung für „{cfg['dhcp_interface']}“ fehlgeschlagen: {net_info['error']} — nichts geschrieben.", "danger")
|
||||
@@ -1927,7 +1955,9 @@ def settings_dhcp():
|
||||
|
||||
elif "dhcp_service_action" in request.form:
|
||||
action = request.form.get("dhcp_service_action")
|
||||
if action == "enable_restart":
|
||||
if action == "enable_restart" and not _dhcp_config_is_complete(_load_dhcp_settings()):
|
||||
flash("Range Start/Ende müssen eingetragen und die Konfiguration geschrieben sein, bevor der Dienst gestartet wird.", "danger")
|
||||
elif action == "enable_restart":
|
||||
ok_enable, _ = _dhcp_run_privileged(["systemctl", "enable", DHCP_SERVICE], timeout=15)
|
||||
ok_restart, restart_out = _dhcp_run_privileged(["systemctl", "restart", DHCP_SERVICE], timeout=30)
|
||||
log_action("settings.dhcp_service", DHCP_SERVICE, "enable+restart " + ("ok" if (ok_enable and ok_restart) else "fehlgeschlagen"))
|
||||
|
||||
@@ -366,6 +366,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- Quelltext-Hürde ----------------
|
||||
Blockiert Rechtsklick-Menü und die üblichen DevTools-/Quelltext-
|
||||
Shortcuts. WICHTIG: das ist eine Hürde für gelegentliches Kopieren,
|
||||
KEIN echter Schutz — jeder Browser liefert HTML/CSS/JS immer
|
||||
vollständig aus, sonst könnte er die Seite nicht darstellen. Wer
|
||||
wirklich an den Quelltext will, umgeht das in Sekunden (URL-Leiste,
|
||||
externer HTTP-Client, DevTools per Menü statt Shortcut). Echte
|
||||
Sicherheit kommt ausschließlich von den serverseitigen Rechteprüfungen
|
||||
in app.py, nicht davon, das Nachsehen im Browser zu erschweren. */
|
||||
function initSourceProtection() {
|
||||
document.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const key = e.key.toUpperCase();
|
||||
const blocked =
|
||||
key === "F12" ||
|
||||
(e.ctrlKey && e.shiftKey && ["I", "J", "C"].includes(key)) ||
|
||||
(e.ctrlKey && key === "U");
|
||||
if (blocked) e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- Init ---------------- */
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
@@ -378,6 +399,7 @@
|
||||
initCheckTimer();
|
||||
initRunCheckNow();
|
||||
initSortableTables();
|
||||
initSourceProtection();
|
||||
document.querySelectorAll("[data-theme-toggle]").forEach((btn) => btn.addEventListener("click", toggleTheme));
|
||||
});
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Navbar-Reihenfolge</h2>
|
||||
<div class="hint">Bestimmt, in welcher Reihenfolge die Menüpunkte inkl. Unterpunkte in der Sidebar erscheinen. Jeder Benutzer sieht davon nur, wofür er auch berechtigt ist.</div>
|
||||
<div class="hint">Reihenfolge der Sidebar-Menüpunkte inkl. Unterpunkte.</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('save_nav_order') }}" id="navOrderForm">
|
||||
|
||||
@@ -123,6 +123,11 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if dhcp_topbar_active is not none %}
|
||||
<span class="timer-pill" title="DHCP-Dienst: {{ 'aktiv' if dhcp_topbar_active else 'inaktiv' }}">
|
||||
<span class="dot" style="animation:none; background:{{ 'var(--success)' if dhcp_topbar_active else 'var(--danger)' }}; box-shadow:none;"></span>DHCP
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if not current_user.is_authenticated %}
|
||||
<a href="{{ url_for('login') }}" class="btn btn-primary">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4"/><path d="M10 17l5-5-5-5"/><path d="M15 12H3"/></svg>
|
||||
|
||||
@@ -11,10 +11,7 @@
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Zugangsdaten</h2>
|
||||
<div class="hint">
|
||||
Zugangsdaten können mehreren Switchen gleichzeitig zugeordnet werden. Beim Anlegen eines
|
||||
Switches lassen sich bestehende Zugangsdaten auswählen oder direkt neue hinterlegen.
|
||||
</div>
|
||||
<div class="hint">Können mehreren Switchen zugleich zugeordnet werden.</div>
|
||||
</div>
|
||||
{% if can_create %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="addCredentialModal">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Geräte</h2>
|
||||
<div class="hint">PoE-Geräte mit IP, MAC-Adresse und zugeordnetem Switch-Port für die automatische Erreichbarkeitsprüfung.</div>
|
||||
<div class="hint">PoE-Geräte mit IP, MAC und zugeordnetem Switch-Port.</div>
|
||||
</div>
|
||||
{% if can_create %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="deviceModal" onclick="resetDeviceForm()">
|
||||
|
||||
@@ -91,12 +91,8 @@
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Gruppen</h2>
|
||||
<div class="hint">
|
||||
Über Gruppen lassen sich Rechte für jeden Bereich der App granular vergeben.
|
||||
Ein Benutzer kann mehreren Gruppen angehören — die Rechte addieren sich. Auf „Rechte“ klicken,
|
||||
um eine Gruppe aufzuklappen und die Berechtigungen im Detail zu sehen bzw. zu ändern.
|
||||
„Admin“ und „Benutzer“ sind feste Systemgruppen und nicht veränderbar. Das Kästchen neben dem
|
||||
Bereichsnamen (Geräte/Logs/Einstellungen) muss gesetzt sein, bevor die Rechte darunter greifen —
|
||||
automatische Neustarts bei Ausfall bleiben von „D“ unberührt. Legende direkt bei den Rechten.
|
||||
Rechte je Gruppe granular vergeben, Mitgliedschaft in mehreren Gruppen addiert sich.
|
||||
„Admin“ und „Benutzer“ sind feste Systemgruppen. Legende direkt bei den Rechten.
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has_permission('groups.create') %}
|
||||
|
||||
@@ -34,11 +34,37 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Hostname</h2>
|
||||
<div class="hint">Name dieses Hosts im Netzwerk.</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has_permission('settings_system.edit') %}
|
||||
<form method="post">
|
||||
<div class="field">
|
||||
<label for="hostname">Hostname</label>
|
||||
<input type="text" name="hostname" id="hostname" value="{{ current_hostname or '' }}" pattern="[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
Hostname setzen
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="field">
|
||||
<label>Hostname</label>
|
||||
<input type="text" value="{{ current_hostname or '' }}" disabled>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" style="grid-column:1 / -1;">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Netzwerkeinstellungen</h2>
|
||||
<div class="hint">IP-Adresse, DNS-Server und DHCP/Statisch-Umschaltung dieses Hosts selbst (nicht zu verwechseln mit dem DHCP-<em>Server</em> für Clients unter „DHCP“).</div>
|
||||
<div class="hint">IP/DNS/DHCP-Umschaltung dieses Hosts (nicht der DHCP-<em>Server</em> unter „DHCP“).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">DHCP-Server-Status</h2>
|
||||
<div class="hint">Rein lesende Erkennung — Installation und Dienststeuerung laufen ausschließlich über die Buttons unten, nie automatisch.</div>
|
||||
<div class="hint">Installation/Dienststeuerung nur über die Buttons unten.</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if status.installed %}
|
||||
@@ -59,7 +59,7 @@
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Netzwerk-Konfiguration</h2>
|
||||
<div class="hint">Subnet, Netzmaske und Gateway werden immer live aus der System-Netzwerkkonfiguration des gewählten Interfaces übernommen — nicht manuell gepflegt, damit sie nie aus dem Ruder laufen können.</div>
|
||||
<div class="hint">Subnet/Netzmaske/Gateway werden live vom gewählten Interface übernommen, nicht manuell gepflegt.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,15 +88,19 @@
|
||||
<div class="field-hint">Subnet/Netzmaske/Gateway oben werden für das hier ausgewählte Interface erkannt.</div>
|
||||
</div>
|
||||
<div class="field"><label>Range Start</label>
|
||||
<input type="text" name="dhcp_range_start" value="{{ cfg.dhcp_range_start }}">
|
||||
<input type="text" name="dhcp_range_start" value="{{ cfg.dhcp_range_start }}" placeholder="z.B. 192.168.1.100" required>
|
||||
</div>
|
||||
<div class="field"><label>Range Ende</label>
|
||||
<input type="text" name="dhcp_range_end" value="{{ cfg.dhcp_range_end }}">
|
||||
<div class="field-hint">Muss innerhalb des oben erkannten Netzes liegen.</div>
|
||||
<input type="text" name="dhcp_range_end" value="{{ cfg.dhcp_range_end }}" placeholder="z.B. 192.168.1.200" required>
|
||||
<div class="field-hint">Pflichtfeld, muss im oben erkannten Netz liegen — ohne Range startet der Dienst nicht.</div>
|
||||
</div>
|
||||
<div class="field"><label>DNS-Server</label>
|
||||
<input type="text" name="dhcp_dns" value="{{ cfg.dhcp_dns }}">
|
||||
<div class="field-hint">Kommagetrennt, falls mehrere. Router/Gateway kommt vom erkannten Netz oben; nur falls dort keins gefunden wird, wird ersatzweise der erste DNS-Eintrag verwendet.</div>
|
||||
<div class="field"><label>Gateway (optional)</label>
|
||||
<input type="text" name="dhcp_gateway" value="{{ cfg.dhcp_gateway }}" placeholder="Automatisch: {{ net_info.gateway or '—' }}">
|
||||
<div class="field-hint">Nur bei abweichendem Router für die Clients nötig — leer lassen für das oben erkannte Gateway.</div>
|
||||
</div>
|
||||
<div class="field"><label>DNS-Server (optional)</label>
|
||||
<input type="text" name="dhcp_dns" value="{{ cfg.dhcp_dns }}" placeholder="z.B. 1.1.1.1, 8.8.8.8">
|
||||
<div class="field-hint">Kommagetrennt, falls mehrere.</div>
|
||||
</div>
|
||||
<div class="field"><label>Domain</label>
|
||||
<input type="text" name="dhcp_domain" value="{{ cfg.dhcp_domain }}">
|
||||
@@ -125,7 +129,7 @@
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Eigene DHCP-Options</h2>
|
||||
<div class="hint">Zusätzliche/herstellerspezifische Options (z.B. Terminal-URL) — global setzbar, pro Client über „Optionen“ bei den Reservierungen unten überschreibbar.</div>
|
||||
<div class="hint">Herstellerspezifische Options, global oder pro Client überschreibbar.</div>
|
||||
</div>
|
||||
{% if can_edit %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="addOptionModal">
|
||||
@@ -187,7 +191,7 @@
|
||||
<div class="hint">
|
||||
{{ reservations|length }} aktive{{ ' Clients' if reservations|length != 1 else 'r Client' }} mit gültiger MAC + IP im konfigurierten Subnet.
|
||||
{% if skipped_count %}{{ skipped_count }} aktive{{ ' Clients' if skipped_count != 1 else 'r Client' }} ohne MAC/IP übersprungen.{% endif %}
|
||||
{% if out_of_subnet_count %}{{ out_of_subnet_count }} aktive{{ ' Clients außerhalb' if out_of_subnet_count != 1 else 'r Client außerhalb' }} des erkannten Netzes{% if net_info.network %} „{{ net_info.network }}“{% endif %} übersprungen — Kea lehnt Reservierungen außerhalb ihres Subnets ab.{% endif %}
|
||||
{% if out_of_subnet_count %}{{ out_of_subnet_count }} aktive{{ ' Clients außerhalb' if out_of_subnet_count != 1 else 'r Client außerhalb' }} des erkannten Netzes übersprungen.{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if can_edit %}
|
||||
@@ -205,7 +209,7 @@
|
||||
<div class="table-wrap" style="margin-bottom:18px;">
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Hostname</th><th>MAC</th><th>IP-Adresse</th><th>Client-Name</th><th style="width:1%;">Aktionen</th></tr></thead>
|
||||
<thead><tr><th>Hostname</th><th>MAC</th><th>IP-Adresse</th><th>Client-Name</th><th>Optionen</th><th style="width:1%;">Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in reservations %}
|
||||
<tr>
|
||||
@@ -213,6 +217,17 @@
|
||||
<td class="mono">{{ r.mac }}</td>
|
||||
<td class="mono">{{ r.ip }}</td>
|
||||
<td>{{ r.name }}</td>
|
||||
<td style="font-size:12px;">
|
||||
{% for d in option_defs %}
|
||||
{% set override = option_values.get(d.id, {}).get(r.mac) %}
|
||||
{% set global_val = option_values.get(d.id, {}).get('') %}
|
||||
{% if override %}
|
||||
<span class="pill user" title="{{ d.name }} = {{ override }}">{{ d.name }} (eigen)</span>
|
||||
{% elif global_val %}
|
||||
<span class="pill" style="background:var(--muted-dim); color:var(--text-faint);" title="{{ d.name }} = {{ global_val }}">{{ d.name }} (global)</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if option_defs and can_edit %}
|
||||
<button class="icon-btn" title="DHCP-Optionen für diesen Client" data-open-modal="deviceOptionsModal{{ loop.index }}">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Export</h2>
|
||||
<div class="hint">Geräte, Switche und Zugangsdaten als verschlüsseltes Bundle sichern — z.B. für einen Umzug auf eine neue Umgebung.</div>
|
||||
<div class="hint">Geräte, Switche und Zugangsdaten verschlüsselt sichern.</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('export_data') }}">
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Import</h2>
|
||||
<div class="hint">Ein zuvor exportiertes Bundle einlesen. Bestehende Einträge mit gleichem Namen/Hostname/MAC werden aktualisiert, neue werden angelegt.</div>
|
||||
<div class="hint">Exportiertes Bundle einlesen — vorhandene Einträge werden aktualisiert, neue angelegt.</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has_permission('settings_importexport.edit') %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Switche</h2>
|
||||
<div class="hint">Aruba-Switche mit ihren Zugangsdaten, über die der automatische PoE-Neustart bei Ausfällen sowie der Verbindungstest laufen.</div>
|
||||
<div class="hint">Aruba-Switche mit ihren Zugangsdaten für PoE-Neustart und Verbindungstest.</div>
|
||||
</div>
|
||||
{% if can_create %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="addSwitchModal" onclick="resetCredentialChoice('add')">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Benutzer</h2>
|
||||
<div class="hint">Benutzerkonten mit Zugriff auf den PoE Manager. Die Gruppe bestimmt, welche Verwaltungsrechte ein Benutzer hat.</div>
|
||||
<div class="hint">Die Gruppe bestimmt die Rechte eines Benutzers.</div>
|
||||
</div>
|
||||
{% if current_user.has_permission('users.create') %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="userModal" onclick="document.getElementById('userForm').reset();">
|
||||
|
||||
Reference in New Issue
Block a user