Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f59ba42e55 | ||
|
|
b1275b2c71 | ||
|
|
1d8f92fd2d |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.40"
|
version = "2.8.43"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -112,9 +112,24 @@ def overview():
|
|||||||
stil: kea.client_konfiguration(conn, stil) for stil, _, _ in kea.CLIENT_STILE
|
stil: kea.client_konfiguration(conn, stil) for stil, _, _ in kea.CLIENT_STILE
|
||||||
},
|
},
|
||||||
sysops_available=bool(sysops and sysops.available),
|
sysops_available=bool(sysops and sysops.available),
|
||||||
|
# "gespeichert" heisst nicht "wirksam": lehnt Kea die Datei ab, bleibt
|
||||||
|
# der Dienst auf dem alten Stand, waehrend die Seite den neuen Wert
|
||||||
|
# zeigt. Deshalb wird hier verglichen, was auf der Platte liegt.
|
||||||
|
konfig_stand=_konfig_stand(config),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _konfig_stand(gewuenscht: dict[str, Any]) -> str:
|
||||||
|
"""``aktuell``, ``abweichend`` oder ``unbekannt``."""
|
||||||
|
sysops = core().sysops
|
||||||
|
if sysops is None or not sysops.available:
|
||||||
|
return "unbekannt"
|
||||||
|
vorhanden = kea.laufende_konfiguration(sysops)
|
||||||
|
if vorhanden is None:
|
||||||
|
return "unbekannt"
|
||||||
|
return "aktuell" if vorhanden == gewuenscht else "abweichend"
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Einstellungen und Subnetze
|
# Einstellungen und Subnetze
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -148,7 +163,13 @@ def add_subnet():
|
|||||||
start = (request.form.get("range_start") or "").strip()
|
start = (request.form.get("range_start") or "").strip()
|
||||||
end = (request.form.get("range_end") or "").strip()
|
end = (request.form.get("range_end") or "").strip()
|
||||||
|
|
||||||
problem = kea.validate_subnet(networks, interface, start, end)
|
problem = kea.validate_subnet(
|
||||||
|
networks,
|
||||||
|
interface,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
vorhandene=Database.all(conn, "SELECT * FROM dhcp_subnets"),
|
||||||
|
)
|
||||||
if problem:
|
if problem:
|
||||||
flash(problem, "danger")
|
flash(problem, "danger")
|
||||||
return redirect(url_for("dhcp.overview"))
|
return redirect(url_for("dhcp.overview"))
|
||||||
@@ -183,7 +204,14 @@ def update_subnet(subnet_id: int):
|
|||||||
interface = (request.form.get("interface") or "").strip()
|
interface = (request.form.get("interface") or "").strip()
|
||||||
start = (request.form.get("range_start") or "").strip()
|
start = (request.form.get("range_start") or "").strip()
|
||||||
end = (request.form.get("range_end") or "").strip()
|
end = (request.form.get("range_end") or "").strip()
|
||||||
problem = kea.validate_subnet(networks, interface, start, end)
|
problem = kea.validate_subnet(
|
||||||
|
networks,
|
||||||
|
interface,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
vorhandene=Database.all(conn, "SELECT * FROM dhcp_subnets"),
|
||||||
|
eigene_id=subnet_id,
|
||||||
|
)
|
||||||
if problem:
|
if problem:
|
||||||
flash(problem, "danger")
|
flash(problem, "danger")
|
||||||
return redirect(url_for("dhcp.overview"))
|
return redirect(url_for("dhcp.overview"))
|
||||||
@@ -608,7 +636,13 @@ def _write_and_redirect():
|
|||||||
for warning in result.get("warnings", []):
|
for warning in result.get("warnings", []):
|
||||||
flash(warning, "warning")
|
flash(warning, "warning")
|
||||||
if not result["ok"] and result.get("message"):
|
if not result["ok"] and result.get("message"):
|
||||||
flash(result["message"], "warning")
|
# Rot, nicht orange: die Aenderung steht in der Datenbank und auf der
|
||||||
|
# Seite, wirkt aber nicht. Das ist der Zustand, in dem man stundenlang
|
||||||
|
# sucht, warum ein Client die Option nicht bekommt.
|
||||||
|
flash(
|
||||||
|
result["message"] + " Die Änderung ist gespeichert, aber noch nicht wirksam.",
|
||||||
|
"danger",
|
||||||
|
)
|
||||||
elif result["ok"]:
|
elif result["ok"]:
|
||||||
flash(result["message"], "info")
|
flash(result["message"], "info")
|
||||||
return redirect(url_for("dhcp.overview"))
|
return redirect(url_for("dhcp.overview"))
|
||||||
|
|||||||
@@ -186,9 +186,20 @@ def matching_network(
|
|||||||
|
|
||||||
|
|
||||||
def validate_subnet(
|
def validate_subnet(
|
||||||
networks: Iterable[InterfaceNetwork], interface: str, range_start: str, range_end: str
|
networks: Iterable[InterfaceNetwork],
|
||||||
|
interface: str,
|
||||||
|
range_start: str,
|
||||||
|
range_end: str,
|
||||||
|
*,
|
||||||
|
vorhandene: Iterable[Any] = (),
|
||||||
|
eigene_id: int | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Rueckgabe: leerer String bei Erfolg, sonst die Fehlermeldung."""
|
"""Rueckgabe: leerer String bei Erfolg, sonst die Fehlermeldung.
|
||||||
|
|
||||||
|
``vorhandene`` sind die schon eingetragenen Bereiche. Ueberschneidet sich
|
||||||
|
der neue mit einem davon, entsteht eine Konfiguration, die Kea als Ganzes
|
||||||
|
ablehnt -- besser hier sagen als spaeter beim Schreiben.
|
||||||
|
"""
|
||||||
if not interface:
|
if not interface:
|
||||||
return "Es ist kein Interface ausgewählt."
|
return "Es ist kein Interface ausgewählt."
|
||||||
try:
|
try:
|
||||||
@@ -203,9 +214,41 @@ def validate_subnet(
|
|||||||
f"Der Bereich {range_start}-{range_end} passt in kein Netz von {interface}. "
|
f"Der Bereich {range_start}-{range_end} passt in kein Netz von {interface}. "
|
||||||
"Subnetz und Maske werden vom laufenden System gelesen."
|
"Subnetz und Maske werden vom laufenden System gelesen."
|
||||||
)
|
)
|
||||||
|
for zeile in vorhandene:
|
||||||
|
if eigene_id is not None and int(zeile["id"]) == eigene_id:
|
||||||
|
continue
|
||||||
|
if str(zeile["interface"]) != interface:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
anfang = int(ipaddress.ip_address(str(zeile["range_start"])))
|
||||||
|
ende = int(ipaddress.ip_address(str(zeile["range_end"])))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if int(start) <= ende and int(end) >= anfang:
|
||||||
|
return (
|
||||||
|
f"Der Bereich überschneidet sich mit {zeile['range_start']}-"
|
||||||
|
f"{zeile['range_end']} auf {interface}. Erweitern Sie den vorhandenen "
|
||||||
|
"Bereich, statt einen zweiten anzulegen."
|
||||||
|
)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def laufende_konfiguration(sysops: SysOps) -> dict[str, Any] | None:
|
||||||
|
"""Die Konfiguration, die tatsaechlich auf der Platte liegt.
|
||||||
|
|
||||||
|
``None`` heisst: nicht lesbar (kein Helfer, Datei fehlt, kein JSON) -- das
|
||||||
|
ist etwas anderes als "weicht ab" und wird auch anders angezeigt.
|
||||||
|
"""
|
||||||
|
result = sysops.read_privileged_file(DEFAULT_CONFIG_PATH)
|
||||||
|
if not result.ok or not (result.stdout or "").strip():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
geladen = json.loads(result.stdout)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
return None
|
||||||
|
return geladen if isinstance(geladen, dict) else None
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Reservierungen
|
# Reservierungen
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -393,8 +436,16 @@ def render_config(
|
|||||||
return entries
|
return entries
|
||||||
|
|
||||||
subnet_entries: list[dict[str, Any]] = []
|
subnet_entries: list[dict[str, Any]] = []
|
||||||
|
# Ein Netz, ein Eintrag. Kea lehnt die **ganze** Konfiguration ab, sobald
|
||||||
|
# derselbe Praefix zweimal darin steht ("subnet with the prefix of
|
||||||
|
# '192.168.82.0/24' already exists") -- und dann laeuft der Dienst
|
||||||
|
# unbemerkt mit dem alten Stand weiter. Zwei Zeilen im selben Netz sind
|
||||||
|
# aber nichts Unsinniges: es sind zwei Bereiche. Sie werden deshalb zu
|
||||||
|
# einem Subnetz mit mehreren Pools zusammengefasst.
|
||||||
|
nach_netz: dict[str, dict[str, Any]] = {}
|
||||||
|
belegte_bereiche: dict[str, list[tuple[int, int]]] = {}
|
||||||
interfaces: set[str] = set()
|
interfaces: set[str] = set()
|
||||||
for index, subnet in enumerate(subnets, start=1):
|
for subnet in subnets:
|
||||||
interface = str(subnet["interface"])
|
interface = str(subnet["interface"])
|
||||||
match = matching_network(
|
match = matching_network(
|
||||||
networks, interface, str(subnet["range_start"]), str(subnet["range_end"])
|
networks, interface, str(subnet["range_start"]), str(subnet["range_end"])
|
||||||
@@ -433,15 +484,40 @@ def render_config(
|
|||||||
entry["option-data"] = option_data(overrides)
|
entry["option-data"] = option_data(overrides)
|
||||||
subnet_reservations.append(entry)
|
subnet_reservations.append(entry)
|
||||||
|
|
||||||
subnet_entries.append(
|
schluessel = str(network)
|
||||||
{
|
eintrag = nach_netz.get(schluessel)
|
||||||
"id": index,
|
if eintrag is None:
|
||||||
"subnet": str(network),
|
eintrag = {
|
||||||
"pools": [{"pool": f"{subnet['range_start']} - {subnet['range_end']}"}],
|
# Die Nummer zaehlt die Eintraege, nicht die Zeilen der
|
||||||
|
# Tabelle -- sonst haette ein uebersprungenes Subnetz eine
|
||||||
|
# Luecke hinterlassen, und Kea verlangt eindeutige IDs.
|
||||||
|
"id": len(subnet_entries) + 1,
|
||||||
|
"subnet": schluessel,
|
||||||
|
"pools": [],
|
||||||
**({"option-data": subnet_options} if subnet_options else {}),
|
**({"option-data": subnet_options} if subnet_options else {}),
|
||||||
"reservations": subnet_reservations,
|
"reservations": subnet_reservations,
|
||||||
}
|
}
|
||||||
|
nach_netz[schluessel] = eintrag
|
||||||
|
belegte_bereiche[schluessel] = []
|
||||||
|
subnet_entries.append(eintrag)
|
||||||
|
elif subnet_options and subnet_options != eintrag.get("option-data", []):
|
||||||
|
warnings.append(
|
||||||
|
f"Für {schluessel} sind mehrere Bereiche mit unterschiedlichem Gateway oder "
|
||||||
|
"DNS eingetragen. Ein Netz kann nur eines haben | es gilt der erste Eintrag."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Auch zwei Pools, die sich ueberschneiden, bringen Kea zu Fall.
|
||||||
|
von, bis = int(ipaddress.ip_address(str(subnet["range_start"]))), int(
|
||||||
|
ipaddress.ip_address(str(subnet["range_end"]))
|
||||||
)
|
)
|
||||||
|
if any(von <= ende and bis >= anfang for anfang, ende in belegte_bereiche[schluessel]):
|
||||||
|
warnings.append(
|
||||||
|
f"Der Bereich {subnet['range_start']}-{subnet['range_end']} überschneidet sich "
|
||||||
|
"mit einem bereits eingetragenen und wurde übersprungen."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
belegte_bereiche[schluessel].append((von, bis))
|
||||||
|
eintrag["pools"].append({"pool": f"{subnet['range_start']} - {subnet['range_end']}"})
|
||||||
|
|
||||||
if not subnet_entries:
|
if not subnet_entries:
|
||||||
warnings.append("Es ist kein gültiges Subnetz konfiguriert | Kea würde nicht starten.")
|
warnings.append("Es ist kein gültiges Subnetz konfiguriert | Kea würde nicht starten.")
|
||||||
@@ -801,7 +877,9 @@ def service_state(sysops: SysOps) -> dict[str, str]:
|
|||||||
fertig = subprocess.run(
|
fertig = subprocess.run(
|
||||||
[pfad, "-V"], capture_output=True, text=True, timeout=5, check=False
|
[pfad, "-V"], capture_output=True, text=True, timeout=5, check=False
|
||||||
)
|
)
|
||||||
state["version"] = (fertig.stdout or "").strip().splitlines()[0].strip()
|
# ``kea-dhcp4 -V`` antwortet mit "3.0.3 (3.0.3 (tarball))" -- auf
|
||||||
|
# einer Kachel steht davon die Nummer, nicht die Herkunft.
|
||||||
|
state["version"] = (fertig.stdout or "").strip().split()[0].strip()
|
||||||
except (OSError, subprocess.SubprocessError, IndexError):
|
except (OSError, subprocess.SubprocessError, IndexError):
|
||||||
state["version"] = ""
|
state["version"] = ""
|
||||||
return state
|
return state
|
||||||
@@ -36,14 +36,13 @@
|
|||||||
{% if section.tiles %}
|
{% if section.tiles %}
|
||||||
<details class="card" data-tile-section="{{ section.key }}"
|
<details class="card" data-tile-section="{{ section.key }}"
|
||||||
data-persist="dash-{{ section.key }}" {% if section.key != 'disabled' %}open{% endif %}>
|
data-persist="dash-{{ section.key }}" {% if section.key != 'disabled' %}open{% endif %}>
|
||||||
<summary class="card__header" style="cursor:pointer">
|
<summary class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">
|
<div class="card__title">
|
||||||
{{ pill(section.tiles | length, section.kind if section.kind != 'muted' else '') }}
|
{{ pill(section.tiles | length, section.kind if section.kind != 'muted' else '') }}
|
||||||
{{ section.label }}
|
{{ section.label }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ icon('chevron-down', size=16) }}
|
|
||||||
</summary>
|
</summary>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<div class="grid grid--tiles">
|
<div class="grid grid--tiles">
|
||||||
|
|||||||
@@ -7,6 +7,29 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{# Der wichtigste Satz der Seite, wenn er zutrifft: was hier steht, ist nicht,
|
||||||
|
womit der Dienst arbeitet. Lehnt Kea eine Datei ab, bleibt der alte Stand
|
||||||
|
aktiv -- die Oberflaeche zeigte bisher trotzdem den neuen Wert, und man
|
||||||
|
sucht den Fehler beim Client. #}
|
||||||
|
{% if konfig_stand == 'abweichend' %}
|
||||||
|
<div class="alert alert--danger">
|
||||||
|
{{ icon('alert', size=18) }}
|
||||||
|
<div class="alert__body flex-1">
|
||||||
|
<div class="alert__title">Der Dienst arbeitet nicht mit dieser Konfiguration</div>
|
||||||
|
Auf der Platte liegt ein anderer Stand als der hier gezeigte – entweder wurde
|
||||||
|
die Datei zuletzt abgelehnt oder seither nichts geschrieben. Änderungen an Optionen
|
||||||
|
und Reservierungen sind gespeichert, aber <strong>nicht wirksam</strong>.
|
||||||
|
{% if current_user.has('dhcp_config.execute') %}
|
||||||
|
<form method="post" action="{{ url_for('dhcp.write_config') }}" class="mt-2" data-no-guard>
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<button class="btn btn--sm btn--primary" type="submit" data-busy-label="Schreibt ...">
|
||||||
|
Jetzt schreiben und Fehler anzeigen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% for warning in warnings %}{{ alert(warning, 'warning') }}{% endfor %}
|
{% for warning in warnings %}{{ alert(warning, 'warning') }}{% endfor %}
|
||||||
{% if not sysops_available %}
|
{% if not sysops_available %}
|
||||||
{{ alert('Systemhelfer nicht verfügbar -- Konfiguration und Dienstzustand können hier nur
|
{{ alert('Systemhelfer nicht verfügbar -- Konfiguration und Dienstzustand können hier nur
|
||||||
@@ -21,8 +44,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat__label">Programm</span>
|
<span class="stat__label">Programm</span>
|
||||||
<span class="stat__value fs-lg">{{ service.get('binary', '?') }}</span>
|
{# Die Fassung statt nur "vorhanden": zwischen Kea 2.4 (Ubuntu 24.04) und
|
||||||
<span class="stat__hint">kea-dhcp4</span>
|
3.0 (26.04) liegen Aenderungen, und wer eine Konfiguration beurteilt,
|
||||||
|
will wissen, wogegen. #}
|
||||||
|
<span class="stat__value fs-lg">{{ service.get('version') or service.get('binary', '?') }}</span>
|
||||||
|
<span class="stat__hint">
|
||||||
|
kea-dhcp4{% if service.get('version') %} · {{ service.get('binary', '?') }}{% endif %}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat__label">Reservierungen</span>
|
<span class="stat__label">Reservierungen</span>
|
||||||
@@ -252,7 +280,7 @@
|
|||||||
Rollbereich innerhalb der scrollenden Seite -- und das Gesuchte ging darin
|
Rollbereich innerhalb der scrollenden Seite -- und das Gesuchte ging darin
|
||||||
unter. Jetzt liegt er hinter einem Klick, mit eigener Suche. #}
|
unter. Jetzt liegt er hinter einem Klick, mit eigener Suche. #}
|
||||||
<details class="card" data-persist="dhcp-katalog">
|
<details class="card" data-persist="dhcp-katalog">
|
||||||
<summary class="card__header" style="cursor:pointer">
|
<summary class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('list', size=17) }} Optionskatalog</div>
|
<div class="card__title">{{ icon('list', size=17) }} Optionskatalog</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
@@ -403,7 +431,7 @@
|
|||||||
{% for stil, bezeichnung, pfad in client_stile %}
|
{% for stil, bezeichnung, pfad in client_stile %}
|
||||||
{% set konfig = client_konfig.get(stil) %}
|
{% set konfig = client_konfig.get(stil) %}
|
||||||
<details class="card__body pt-0">
|
<details class="card__body pt-0">
|
||||||
<summary class="row row--tight" style="cursor:pointer">
|
<summary class="row row--tight">
|
||||||
<strong class="fs-sm">{{ bezeichnung }}</strong>
|
<strong class="fs-sm">{{ bezeichnung }}</strong>
|
||||||
<span class="fs-xs faint mono">{{ pfad }}</span>
|
<span class="fs-xs faint mono">{{ pfad }}</span>
|
||||||
</summary>
|
</summary>
|
||||||
@@ -629,12 +657,11 @@
|
|||||||
|
|
||||||
{# -- Vorschau ---------------------------------------------------------- #}
|
{# -- Vorschau ---------------------------------------------------------- #}
|
||||||
<details class="card" data-persist="dhcp-preview">
|
<details class="card" data-persist="dhcp-preview">
|
||||||
<summary class="card__header" style="cursor:pointer">
|
<summary class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('file-text', size=17) }} Erzeugte Kea-Konfiguration</div>
|
<div class="card__title">{{ icon('file-text', size=17) }} Erzeugte Kea-Konfiguration</div>
|
||||||
<div class="card__sub">Vorschau – so würde die Datei geschrieben</div>
|
<div class="card__sub">Vorschau – so würde die Datei geschrieben</div>
|
||||||
</div>
|
</div>
|
||||||
{{ icon('chevron-down', size=16) }}
|
|
||||||
</summary>
|
</summary>
|
||||||
<pre class="log-view">{{ preview }}</pre>
|
<pre class="log-view">{{ preview }}</pre>
|
||||||
{% if current_user.has('dhcp_config.execute') %}
|
{% if current_user.has('dhcp_config.execute') %}
|
||||||
|
|||||||
@@ -100,6 +100,10 @@ check_readable_path() {
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
/var/log/kea/kea-dhcp4.log) ;;
|
/var/log/kea/kea-dhcp4.log) ;;
|
||||||
/var/lib/kea/kea-leases4.csv) ;;
|
/var/lib/kea/kea-leases4.csv) ;;
|
||||||
|
# Die eigene Kea-Konfiguration: 0640 root:_kea, also fuer den
|
||||||
|
# Dienstbenutzer nicht lesbar. Die Anwendung braucht sie, um zu sehen, ob
|
||||||
|
# der Dienst ueberhaupt mit dem laeuft, was sie geschrieben hat.
|
||||||
|
/etc/kea/kea-dhcp4.conf) ;;
|
||||||
/etc/netplan/90-*.yaml)
|
/etc/netplan/90-*.yaml)
|
||||||
check_app_key "$(basename "$1" .yaml | sed 's/^90-//')" ;;
|
check_app_key "$(basename "$1" .yaml | sed 's/^90-//')" ;;
|
||||||
/etc/nginx/sites-available/*)
|
/etc/nginx/sites-available/*)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.8.20"
|
version = "2.8.21"
|
||||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -1131,6 +1131,43 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
/* ---------------------------------------------------------------------- #
|
||||||
|
Aufklappbares
|
||||||
|
---------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
Der native Marker eines <summary> verschwindet, sobald das Element ein
|
||||||
|
Flex-Container ist -- und genau das sind unsere Kartenkoepfe. Auf der
|
||||||
|
DHCP-Seite stand der Optionskatalog deshalb als Kasten da, dem man nicht
|
||||||
|
ansah, dass er sich oeffnen laesst.
|
||||||
|
|
||||||
|
Der Pfeil wird deshalb selbst gezeichnet: als Flex- bzw. Inline-Element,
|
||||||
|
das in beiden Faellen erscheint, in Akzentfarbe (das ist die Farbe, mit
|
||||||
|
der diese Oberflaeche "hier kannst du etwas tun" sagt) und mit einer
|
||||||
|
Drehung beim Oeffnen. Eine Stelle fuer alle Aufklapper -- Karten,
|
||||||
|
Vorschauen, Abschnitte. */
|
||||||
|
details > summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
details > summary::-webkit-details-marker { display: none; }
|
||||||
|
details:not(.nav-tree) > summary::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin-right: var(--sp-2);
|
||||||
|
border-left: 6px solid var(--accent);
|
||||||
|
border-top: 5px solid transparent;
|
||||||
|
border-bottom: 5px solid transparent;
|
||||||
|
transition: transform var(--dur) var(--ease);
|
||||||
|
}
|
||||||
|
details:not(.nav-tree)[open] > summary::before { transform: rotate(90deg); }
|
||||||
|
/* Der Navigationsbaum bringt seinen eigenen Winkel mit -- der bekommt
|
||||||
|
dieselbe Farbe, damit "aufklappbar" ueberall gleich aussieht. */
|
||||||
|
.nav-tree__chevron { color: var(--accent); }
|
||||||
|
|
||||||
.log-view mark { background: var(--warning-soft); color: var(--warning); border-radius: 3px; }
|
.log-view mark { background: var(--warning-soft); color: var(--warning); border-radius: 3px; }
|
||||||
|
|
||||||
/* Stufen im laufenden Protokoll. Die Zuordnung der Farbrollen ist Absicht:
|
/* Stufen im laufenden Protokoll. Die Zuordnung der Farbrollen ist Absicht:
|
||||||
|
|||||||
@@ -73,12 +73,11 @@ blieb dadurch über Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<details class="card" data-persist="nginx-preview">
|
<details class="card" data-persist="nginx-preview">
|
||||||
<summary class="card__header" style="cursor:pointer">
|
<summary class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('file-text', size=17) }} Erzeugte nginx-Konfiguration</div>
|
<div class="card__title">{{ icon('file-text', size=17) }} Erzeugte nginx-Konfiguration</div>
|
||||||
<div class="card__sub">Vorschau der Datei, die geschrieben würde</div>
|
<div class="card__sub">Vorschau der Datei, die geschrieben würde</div>
|
||||||
</div>
|
</div>
|
||||||
{{ icon('chevron-down', size=16) }}
|
|
||||||
</summary>
|
</summary>
|
||||||
<pre class="log-view">{{ preview }}</pre>
|
<pre class="log-view">{{ preview }}</pre>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
Reference in new issue
Block a user