Files
tesm/srv/tesm/templates/settings_dhcp.html
T
alientim 33753e4f41 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.
2026-08-14 12:05:39 +02:00

664 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% set active_page = "settings_dhcp" %}
{% set can_edit = current_user.has_permission('settings_dhcp.edit') %}
{% block page_title %}DHCP{% endblock %}
{% block page_sub %}<div class="topbar-sub">Reservierungen und eigene Options aus den Client-Stammdaten (Kea DHCP)</div>{% endblock %}
{% block content %}
<div class="settings-grid">
<div class="card card-pad">
<div class="section-head" style="margin-bottom:16px;">
<div>
<h2 style="font-size:16px;">DHCP-Server-Status</h2>
<div class="hint">Installation/Dienststeuerung nur über die Buttons unten.</div>
</div>
</div>
{% if status.installed %}
<div class="flex gap-2" style="align-items:center; margin-bottom:10px; flex-wrap:wrap;">
<span class="pill online">Installiert</span>
{% if status.active is sameas true %}
<span class="pill online">Dienst aktiv</span>
{% elif status.active is sameas false %}
<span class="pill offline">Dienst inaktiv</span>
{% else %}
<span class="pill unknown">Dienst-Status unbekannt</span>
{% endif %}
{% if not can_edit and status.enabled is sameas true %}<span class="pill user">Autostart an</span>{% endif %}
</div>
{% if status.version %}<div class="text-faint mono" style="font-size:12px; margin-bottom:12px;">{{ status.version }}</div>{% endif %}
{% if can_edit %}
<div class="flex gap-2" style="align-items:center; margin-bottom:14px;">
<label class="switch-check">
<input type="checkbox" id="dhcpAutostartToggle" {% if status.enabled %}checked{% endif %} onchange="toggleDhcpAutostart(this)">
<span class="track"></span>
</label>
<span style="font-size:13px;">Autostart beim Hochfahren des Hosts</span>
</div>
<div class="flex gap-2" style="flex-wrap:wrap;">
<form method="post" data-confirm="{{ dhcp_service }} aktivieren und (neu) starten? Übernimmt die zuletzt geschriebene Konfiguration.">
<input type="hidden" name="dhcp_service_action" value="enable_restart">
<button type="submit" class="btn btn-secondary btn-sm">Aktivieren &amp; (neu) starten</button>
</form>
{% if status.active is sameas true %}
<form method="post" data-confirm="{{ dhcp_service }} stoppen und deaktivieren? Startet nach einem Host-Neustart dann nicht automatisch wieder.">
<input type="hidden" name="dhcp_service_action" value="stop">
<button type="submit" class="btn btn-secondary btn-sm" style="color:var(--danger);">Stoppen</button>
</form>
{% endif %}
</div>
{% endif %}
{% else %}
<span class="pill disabled">Nicht installiert</span>
<p class="text-faint" style="font-size:11.5px; margin:10px 0;">
Kein <code>kea-dhcp4</code> auf diesem Host gefunden.
</p>
{% if can_edit %}
<form method="post" data-confirm="{{ dhcp_package }} jetzt installieren (apt-get)? Startet den Dienst noch nicht.">
<input type="hidden" name="install_dhcp_package" value="1">
<button type="submit" class="btn btn-primary btn-sm">{{ dhcp_package }} installieren</button>
</form>
{% endif %}
{% endif %}
</div>
<div class="card card-pad">
<div class="section-head" style="margin-bottom:16px;">
<div>
<h2 style="font-size:16px;">Globale DHCP-Einstellungen</h2>
<div class="hint">Gelten für den gesamten Dienst, unabhängig von den Subnetzen unten.</div>
</div>
</div>
{% if can_edit %}
<form method="post">
<input type="hidden" name="save_dhcp_config" value="1">
<div class="field"><label>Domain</label>
<input type="text" name="dhcp_domain" value="{{ cfg.dhcp_domain }}">
</div>
<div class="field"><label>Lease-Zeit Standard (Sek.)</label>
<input type="number" name="dhcp_lease_default" value="{{ cfg.dhcp_lease_default }}">
</div>
<div class="field"><label>Lease-Zeit Maximum (Sek.)</label>
<input type="number" name="dhcp_lease_max" value="{{ cfg.dhcp_lease_max }}">
</div>
<div class="field"><label>Ausgabepfad (Kea-Konfigurationsdatei)</label>
<input type="text" name="dhcp_output_path" value="{{ cfg.dhcp_output_path }}" class="mono">
<div class="field-hint">Standardmäßig die aktive Kea-Konfiguration. Läuft der Dienst gerade, schreibt „Speichern“ die Datei neu und startet ihn automatisch neu.</div>
</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>
Konfiguration speichern
</button>
</form>
{% else %}
<p class="text-faint" style="font-size:12.5px;">Für Änderungen fehlt das Recht „DHCP ändern“.</p>
{% 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;">Subnetze</h2>
<div class="hint">
Ein Host kann mehrere IPs/Interfaces mit jeweils eigenem Netz bedienen — je Subnetz eigene Range,
optional eigenes Gateway/DNS. Subnet/Netzmaske werden live vom Interface übernommen, nicht manuell gepflegt.
</div>
</div>
{% if can_edit %}
<button type="button" class="btn btn-primary" data-open-modal="addSubnetModal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
Subnetz hinzufügen
</button>
{% endif %}
</div>
{% if subnets %}
<div class="table-wrap">
<div style="overflow-x:auto;">
<table class="data-table">
<thead><tr><th>Interface</th><th>Range</th><th>Gateway</th><th>DNS</th><th>Erkanntes Netz</th><th style="width:1%;">Aktionen</th></tr></thead>
<tbody>
{% for s in subnets %}
{% set st = subnet_status.get(s.id) %}
<tr {% if not s.enabled %}style="opacity:.55;"{% endif %}>
<td class="mono">{{ s.interface }}</td>
<td class="mono">{{ s.range_start }} {{ s.range_end }}</td>
<td class="mono text-faint">{{ s.gateway or '(automatisch)' }}</td>
<td class="mono text-faint">{{ s.dns or '—' }}</td>
<td>
{% if not s.enabled %}
<span class="pill disabled">Deaktiviert</span>
{% elif st and st.ok %}
<span class="pill online">Erkannt</span>
<span class="mono text-faint" style="font-size:11.5px;">{{ st.network }}</span>
{% else %}
<span class="pill offline">Nicht erkannt</span>
{% if st %}<span class="text-faint" style="font-size:11px;">{{ st.error }}</span>{% endif %}
{% endif %}
</td>
<td>
{% if can_edit %}
<div class="flex gap-2">
<form method="post">
<input type="hidden" name="toggle_dhcp_subnet" value="{{ s.id }}">
<button type="submit" class="icon-btn" title="{{ 'Deaktivieren' if s.enabled else 'Aktivieren' }}">
{% if s.enabled %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M4.9 4.9l14.2 14.2"/></svg>
{% else %}
<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>
{% endif %}
</button>
</form>
<button type="button" class="icon-btn" title="Bearbeiten" data-open-modal="editSubnetModal{{ loop.index }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4z"/></svg>
</button>
<form method="post" data-confirm="Subnetz {{ s.range_start }}{{ s.range_end }} löschen?">
<input type="hidden" name="delete_dhcp_subnet" value="{{ s.id }}">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<p class="text-faint" style="font-size:12.5px;">Noch kein Subnetz konfiguriert — der Dienst kann so nicht gestartet werden.</p>
{% 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;">DHCP-Options</h2>
<div class="hint">
Standard-Optionen (analog den "Predefined Options" eines Windows-DHCP-Servers) sind bereits vorbefüllt,
eigene/herstellerspezifische Options lassen sich zusätzlich anlegen — beide global oder pro Client überschreibbar.
</div>
</div>
{% if can_edit %}
<button type="button" class="btn btn-primary" data-open-modal="addOptionModal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
Neue Option
</button>
{% endif %}
</div>
{% set visible_option_defs = option_defs|rejectattr("is_standard")|list + option_defs|selectattr("is_standard")|selectattr("id", "in", option_values.keys()|list)|list %}
{% if visible_option_defs %}
<div class="table-wrap">
<div style="overflow-x:auto;">
<table class="data-table">
<thead><tr><th>Code</th><th>Name</th><th>Typ</th><th>Herkunft</th><th>Beschreibung</th><th>Globaler Wert</th><th style="width:1%;">Aktionen</th></tr></thead>
<tbody>
{% for d in visible_option_defs|sort(attribute="code") %}
<tr>
<td class="mono">{{ d.code }}</td>
<td class="mono">{{ d.name }}</td>
<td>{{ d.type }}</td>
<td>
{% if d.is_standard %}
<span class="pill" style="background:var(--muted-dim); color:var(--text-faint);">Standard</span>
{% else %}
<span class="pill user">Eigen</span>
{% endif %}
</td>
<td class="text-faint" style="font-size:12px;">{{ d.description or '—' }}</td>
<td>
{% if can_edit %}
<form method="post" class="flex gap-2">
<input type="hidden" name="save_dhcp_option_global" value="1">
<input type="hidden" name="option_def_id" value="{{ d.id }}">
<input type="text" name="value" value="{{ option_values.get(d.id, {}).get('', '') }}" style="max-width:220px;" placeholder="(nicht gesetzt)">
<button type="submit" class="btn btn-secondary btn-sm">Speichern</button>
</form>
<div class="field-hint">Leeren Wert speichern, um eine Standard-Option wieder auszublenden.</div>
{% else %}
{{ option_values.get(d.id, {}).get('', '—') }}
{% endif %}
</td>
<td>
{% if can_edit and not d.is_standard %}
<form method="post" data-confirm="Option „{{ d.name }}“ inkl. aller Werte/Overrides löschen?">
<input type="hidden" name="delete_dhcp_option" value="{{ d.id }}">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
{% elif d.is_standard %}
<span class="text-faint" title="Standard-Optionen können nicht gelöscht werden">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<p class="text-faint" style="font-size:12.5px;">Noch keine Options in Verwendung.</p>
{% endif %}
{% if can_edit %}
{% set unused_standard_defs = option_defs|selectattr("is_standard")|rejectattr("id", "in", option_values.keys()|list)|sort(attribute="code")|list %}
{% if unused_standard_defs %}
<form method="post" class="flex gap-2" style="margin-top:14px; align-items:flex-end; flex-wrap:wrap;">
<input type="hidden" name="save_dhcp_option_global" value="1">
<div class="field" style="margin:0; min-width:260px;">
<label>Standard-Option hinzufügen</label>
<select name="option_def_id">
{% for d in unused_standard_defs %}
<option value="{{ d.id }}">{{ d.name }} (Code {{ d.code }}) — {{ d.description }}</option>
{% endfor %}
</select>
</div>
<div class="field" style="margin:0;">
<label>Wert</label>
<input type="text" name="value" placeholder="Wert eintragen…" required>
</div>
<button type="submit" class="btn btn-secondary">Hinzufügen</button>
</form>
{% endif %}
{% 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;">Aktive Leases</h2>
<div class="hint">
Direkt aus Kea gelesen (nicht aus den Reservierungen) — zeigt auch Clients OHNE eigene Reservierung,
die sich einfach eine freie IP aus dem Pool genommen haben.
</div>
</div>
</div>
{% if leases is none %}
<p class="text-faint" style="font-size:12.5px;">
Keine Lease-Datei gefunden — der Dienst wurde vermutlich noch nie gestartet oder hat noch keine Adresse vergeben.
</p>
{% elif leases %}
<div class="table-wrap">
<div style="overflow-x:auto;">
<table class="data-table">
<thead><tr><th>IP-Adresse</th><th>MAC</th><th>Hostname</th><th>Läuft ab</th><th>Status</th>{% if can_edit %}<th style="width:1%;">Aktionen</th>{% endif %}</tr></thead>
<tbody>
{% for l in leases %}
<tr>
<td class="mono">{{ l.ip }}</td>
<td class="mono">{{ l.mac }}</td>
<td>{{ l.hostname or '—' }}</td>
<td class="text-faint mono" style="font-size:12px;">
{% if l.expires_at %}
{{ l.expires_at }} UTC
<br><span class="lease-countdown" data-expire-ms="{{ l.expire_epoch_ms }}"></span>
{% else %}
{% endif %}
</td>
<td>
{% if l.reserved %}
<span class="pill online">Reserviert</span>
{% else %}
<span class="pill unknown" title="Client hat sich diese IP selbst aus dem Pool geholt, ohne eigene Reservierung">
⚠ Ohne Reservierung
</span>
{% endif %}
</td>
{% if can_edit %}
<td>
<form method="post" action="{{ url_for('delete_dhcp_lease') }}" data-confirm="Lease {{ l.ip }} ({{ l.mac }}) löschen? Der DHCP-Dienst wird dabei kurz neu gestartet, falls er gerade läuft.">
<input type="hidden" name="ip" value="{{ l.ip }}">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Lease löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6h16z"/><path d="M10 11v6M14 11v6"/></svg>
</button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<p class="text-faint" style="font-size:12.5px;">Aktuell keine aktiven Leases vergeben.</p>
{% 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;">Reservierungen aus den Clients</h2>
<div class="hint">
{{ reservations|length }} aktive{{ ' Clients' if reservations|length != 1 else 'r Client' }} mit gültiger MAC + IP in einem der konfigurierten Subnetze.
{% 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' }} aller konfigurierten Subnetze übersprungen.{% endif %}
</div>
</div>
{% if can_edit %}
<div class="flex gap-2">
<button type="button" class="btn btn-secondary" data-open-modal="addReservationModal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
Reservierung hinzufügen
</button>
<form method="post" data-confirm="Konfiguration nach „{{ cfg.dhcp_output_path }}“ schreiben? Läuft der Dienst gerade, wird er dabei automatisch neu gestartet, damit die Änderung sofort wirksam wird.">
<input type="hidden" name="write_dhcp_file" value="1">
<button type="submit" 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="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>
Speichern
</button>
</form>
</div>
{% endif %}
</div>
{% if reservations %}
<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>Name</th><th>Quelle</th><th>Subnetz</th><th>Optionen</th><th style="width:1%;">Aktionen</th></tr></thead>
<tbody>
{% for r in reservations %}
<tr>
<td class="mono">{{ r.hostname }}</td>
<td class="mono">{{ r.mac }}</td>
<td class="mono">{{ r.ip }}</td>
<td>{{ r.name }}</td>
<td>
{% if r.source == 'manual' %}
<span class="pill user">Manuell</span>
{% else %}
<span class="pill" style="background:var(--muted-dim); color:var(--text-faint);">Auto (Client)</span>
{% endif %}
</td>
<td class="mono text-faint" style="font-size:12px;">{{ r.subnet_label }}</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>
<div class="flex gap-2">
{% if option_defs and can_edit %}
<button type="button" class="icon-btn" title="DHCP-Optionen für diesen Client" data-open-modal="deviceOptionsModal{{ loop.index }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h9M17 6h3M4 12h3M11 12h9M4 18h13M20 18h0"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="17" cy="18" r="2"/></svg>
</button>
{% endif %}
{% if can_edit and r.source == 'manual' %}
<form method="post" data-confirm="Manuelle Reservierung für {{ r.mac }} löschen?">
<input type="hidden" name="delete_dhcp_reservation" value="{{ r.manual_id }}">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
{% elif r.source == 'auto' %}
<span class="text-faint" title="Automatische Reservierungen entstehen aus den Client-Stammdaten und können hier nicht gelöscht werden">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
</span>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if option_defs and can_edit %}
{% for r in reservations %}
<div class="modal-overlay" id="deviceOptionsModal{{ loop.index }}">
<div class="modal">
<form method="post">
<input type="hidden" name="save_dhcp_device_options" value="1">
<input type="hidden" name="mac" value="{{ r.mac }}">
<div class="modal-header">
<h3>DHCP-Optionen — {{ r.name }}</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<p class="text-faint" style="font-size:11.5px; margin:0 0 14px;">
Nur Options mit gesetztem Override sind sichtbar — über das Dropdown weitere hinzufügen.
Leerer Wert übernimmt den globalen Wert (bzw. entfernt den Override).
</p>
<select onchange="poeShowOptionField(this)">
<option value="">+ Option hinzufügen…</option>
{% for d in option_defs %}
{% if not option_values.get(d.id, {}).get(r.mac) %}
<option value="opt-field-{{ d.id }}-{{ loop.index }}-{{ r.mac }}">{{ d.name }} (Code {{ d.code }})</option>
{% endif %}
{% endfor %}
</select>
<div class="poe-added-options" style="margin:10px 0;">
{% for d in option_defs %}
{% set current = option_values.get(d.id, {}).get(r.mac, '') %}
{% if current %}
<div class="field" id="opt-field-{{ d.id }}-{{ loop.index }}-{{ r.mac }}">
<label>{{ d.name }} <span class="text-faint">(Code {{ d.code }})</span></label>
<input type="text" name="opt_{{ d.id }}" value="{{ current }}"
placeholder="{{ option_values.get(d.id, {}).get('', '(kein globaler Wert)') }}">
</div>
{% endif %}
{% endfor %}
</div>
{% for d in option_defs %}
{% set current = option_values.get(d.id, {}).get(r.mac, '') %}
{% if not current %}
<div class="field" id="opt-field-{{ d.id }}-{{ loop.index }}-{{ r.mac }}" style="display:none;">
<label>{{ d.name }} <span class="text-faint">(Code {{ d.code }})</span></label>
<input type="text" name="opt_{{ d.id }}" value=""
placeholder="{{ option_values.get(d.id, {}).get('', '(kein globaler Wert)') }}">
</div>
{% endif %}
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
{% endfor %}
{% endif %}
{% else %}
<p class="text-faint" style="font-size:12.5px; margin-bottom:18px;">Keine aktiven Clients mit MAC + IP vorhanden.</p>
{% endif %}
<div class="permission-group-title">Generierte Konfiguration (Vorschau, Kea-JSON)</div>
<pre class="code-preview">{{ preview }}</pre>
</div>
</div>
{% if can_edit %}
<div class="modal-overlay" id="addOptionModal">
<div class="modal" style="max-width:1000px;">
<form method="post">
<input type="hidden" name="add_dhcp_option" value="1">
<div class="modal-header">
<h3>Neue DHCP-Option</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="field"><label>Code</label>
<input type="number" name="code" min="1" max="254" required placeholder="z.B. 225">
</div>
<div class="field"><label>Name</label>
<input type="text" name="name" required placeholder="z.B. terminal-url" pattern="[a-z][a-z0-9-]*">
<div class="field-hint">Nur Kleinbuchstaben, Ziffern und Bindestrich, muss mit einem Buchstaben beginnen.</div>
</div>
<div class="field"><label>Datentyp</label>
<select name="type">
{% for key, label in option_types %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="field"><label>Beschreibung</label>
<input type="text" name="description" placeholder="z.B. Terminal-Boot-URL für Thin Clients">
</div>
<div class="field"><label>Globaler Wert (optional)</label>
<input type="text" name="initial_value" placeholder="z.B. http://bde/webmmi?M=SGM11">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Anlegen</button>
</div>
</form>
</div>
</div>
<div class="modal-overlay" id="addReservationModal">
<div class="modal">
<form method="post">
<input type="hidden" name="add_dhcp_reservation" value="1">
<div class="modal-header">
<h3>Reservierung hinzufügen</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<p class="text-faint" style="font-size:11.5px; margin:0 0 14px;">
Für Geräte, die nicht als Client in dieser App gepflegt werden (z.B. ein externes Gerät) — bewusst
eine eigene, manuelle Aktion, damit keine unüberwachten Clients automatisch eine feste IP bekommen.
</p>
<div class="field"><label>MAC-Adresse</label>
<input type="text" name="mac" required placeholder="z.B. AA:BB:CC:DD:EE:FF">
</div>
<div class="field"><label>IP-Adresse</label>
<input type="text" name="ip" required placeholder="z.B. 192.168.1.50">
<div class="field-hint">Muss in einem der oben konfigurierten Subnetze liegen, sonst wird sie beim Schreiben übersprungen.</div>
</div>
<div class="field"><label>Name</label>
<input type="text" name="name" required placeholder="z.B. Empfangsdrucker">
<div class="field-hint">Wird zum Hostname der Reservierung. DHCP-Optionen (z.B. Hostname erzwingen) lassen sich danach über das Options-Symbol in der Tabelle setzen.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
</div>
</div>
<div class="modal-overlay" id="addSubnetModal">
<div class="modal">
<form method="post">
<input type="hidden" name="add_dhcp_subnet" value="1">
<div class="modal-header">
<h3>Subnetz hinzufügen</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="field"><label>Interface</label>
<select name="interface" required>
{% for iface in interfaces %}
<option value="{{ iface }}">{{ iface }}</option>
{% endfor %}
</select>
</div>
<div class="field"><label>Range Start</label>
<input type="text" name="range_start" required placeholder="z.B. 192.168.1.100">
</div>
<div class="field"><label>Range Ende</label>
<input type="text" name="range_end" required placeholder="z.B. 192.168.1.200">
<div class="field-hint">Muss im tatsächlich am Interface erkannten Netz liegen — sonst wird das Subnetz abgelehnt.</div>
</div>
<div class="field"><label>Gateway (optional)</label>
<input type="text" name="gateway" placeholder="Automatisch: erkanntes Gateway des Interfaces">
</div>
<div class="field"><label>DNS-Server (optional)</label>
<input type="text" name="dns" placeholder="z.B. 1.1.1.1, 8.8.8.8">
<div class="field-hint">Kommagetrennt, falls mehrere.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
</div>
</div>
{% endif %}
{% if can_edit %}
{% for s in subnets %}
<div class="modal-overlay" id="editSubnetModal{{ loop.index }}">
<div class="modal">
<form method="post">
<input type="hidden" name="edit_dhcp_subnet" value="{{ s.id }}">
<div class="modal-header">
<h3>Subnetz bearbeiten</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="field"><label>Interface</label>
<select name="interface" required>
{% for iface in interfaces %}
<option value="{{ iface }}" {% if iface == s.interface %}selected{% endif %}>{{ iface }}</option>
{% endfor %}
</select>
</div>
<div class="field"><label>Range Start</label>
<input type="text" name="range_start" value="{{ s.range_start }}" required>
</div>
<div class="field"><label>Range Ende</label>
<input type="text" name="range_end" value="{{ s.range_end }}" required>
<div class="field-hint">Muss im tatsächlich am Interface erkannten Netz liegen — sonst wird das Subnetz abgelehnt.</div>
</div>
<div class="field"><label>Gateway (optional)</label>
<input type="text" name="gateway" value="{{ s.gateway or '' }}" placeholder="Automatisch: erkanntes Gateway des Interfaces">
</div>
<div class="field"><label>DNS-Server (optional)</label>
<input type="text" name="dns" value="{{ s.dns or '' }}" placeholder="z.B. 1.1.1.1, 8.8.8.8">
<div class="field-hint">Kommagetrennt, falls mehrere.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
{% endfor %}
{% endif %}
{% endblock %}
{% block scripts %}
<script>
function toggleDhcpAutostart(checkbox) {
checkbox.disabled = true;
fetch("{{ url_for('toggle_dhcp_autostart') }}", { method: "POST" })
.then(r => r.json())
.then(data => {
checkbox.disabled = false;
if (data.success) {
checkbox.checked = data.new_status === 1;
showToast(data.msg, "success");
} else {
checkbox.checked = !checkbox.checked;
showToast(data.msg, "danger");
}
})
.catch(() => { checkbox.disabled = false; checkbox.checked = !checkbox.checked; showToast("Fehler beim Umschalten.", "danger"); });
}
</script>
{% endblock %}