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:
2026-08-11 11:28:29 +02:00
co-authored by Claude Sonnet 5
parent 82edf7628b
commit 6803929140
13 changed files with 191 additions and 81 deletions
+26 -11
View File
@@ -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 }}">