diff --git a/srv/tesm/VERSION b/srv/tesm/VERSION index 512a1fa..5ed5faa 100644 --- a/srv/tesm/VERSION +++ b/srv/tesm/VERSION @@ -1 +1 @@ -1.1.9 +1.1.10 diff --git a/srv/tesm/static/css/style.css b/srv/tesm/static/css/style.css index 3a2cbe3..9cb8f76 100644 --- a/srv/tesm/static/css/style.css +++ b/srv/tesm/static/css/style.css @@ -968,6 +968,48 @@ select { flex-direction: column; animation: modal-in 0.16s ease; } + +/* ---- Hinweis-Icons (Hover-Tooltip + Klick-Modal), siehe _hint_icon.html + und initHintIcons() in app.js ---- */ +.hint-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 15px; + height: 15px; + flex: 0 0 auto; + border-radius: 50%; + border: 1.5px solid var(--text-faint); + background: transparent; + color: var(--text-faint); + font-size: 10px; + font-weight: 700; + font-style: italic; + font-family: Georgia, "Times New Roman", serif; + line-height: 1; + cursor: help; + vertical-align: middle; + margin-left: 6px; + padding: 0; + transition: border-color 0.15s, color 0.15s; +} +.hint-icon:hover, .hint-icon:focus-visible { border-color: var(--accent); color: var(--accent); } + +#hint-tooltip { + position: fixed; + z-index: 10000; + max-width: 280px; + padding: 8px 12px; + border-radius: 8px; + font-size: 12px; + line-height: 1.5; + background: var(--bg-elevated); + color: var(--text); + border: 1px solid var(--border); + box-shadow: var(--shadow); + pointer-events: none; + display: none; +} @keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } diff --git a/srv/tesm/static/js/app.js b/srv/tesm/static/js/app.js index 1555e80..b10e85f 100644 --- a/srv/tesm/static/js/app.js +++ b/srv/tesm/static/js/app.js @@ -285,6 +285,102 @@ .catch(() => { content.textContent = "Fehler beim Laden des Logs."; }); }; + /* ---------------- Hinweis-Icons (Hover-Tooltip + Klick-Modal) ---------------- */ + /* Zentraler Mechanismus für ALLE "i"-Icons (siehe _hint_icon.html) app-weit + -- ein einziges delegiertes Set von Listenern statt pro Icon eigener + Handler, damit auch nachträglich per JS eingefügte Icons (z.B. in + dynamisch nachgeladenen Tabellenzeilen) ohne weiteres Zutun funktionieren. + Hover zeigt den Text als Tooltip neben dem Mauszeiger -- aber NUR auf + Geräten, die tatsächlich sinnvoll hovern können (matchMedia-Check bei + JEDEM mouseenter neu ausgewertet, nicht einmalig beim Laden gecacht, + damit z.B. ein Convertible/Tablet beim Umschalten Maus/Touch korrekt + reagiert). Auf reinen Touch-Geräten bleibt so nur Tap -> Modal übrig, + da dort ohnehin kein echtes mouseenter/mousemove-Hover stattfindet. */ + function ensureHintTooltip() { + if (document.getElementById("hint-tooltip")) return; + const el = document.createElement("div"); + el.id = "hint-tooltip"; + document.body.appendChild(el); + } + + function ensureHintModal() { + if (document.getElementById("hint-modal")) return; + const html = ` + `; + document.body.insertAdjacentHTML("beforeend", html); + document.querySelectorAll("#hint-modal [data-close-modal]").forEach((btn) => { + btn.addEventListener("click", () => closeModal(btn)); + }); + document.getElementById("hint-modal").addEventListener("click", (e) => { + if (e.target.id === "hint-modal") closeModal(e.target); + }); + } + + function canHover() { + return window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches; + } + + function initHintIcons() { + if (!document.querySelector(".hint-icon")) return; + ensureHintTooltip(); + const tooltip = document.getElementById("hint-tooltip"); + + function positionTooltip(e) { + const offset = 14; + const rect = tooltip.getBoundingClientRect(); + let x = e.clientX + offset; + let y = e.clientY + offset; + if (x + rect.width > window.innerWidth - 8) x = e.clientX - rect.width - offset; + if (y + rect.height > window.innerHeight - 8) y = e.clientY - rect.height - offset; + tooltip.style.left = Math.max(8, x) + "px"; + tooltip.style.top = Math.max(8, y) + "px"; + } + + // mouseenter/mouseleave bubbeln nicht -- Delegation via Capture-Phase + // auf document funktioniert dafür trotzdem zuverlässig. + document.addEventListener("mouseenter", function (e) { + const icon = e.target.closest && e.target.closest(".hint-icon"); + if (!icon || !canHover()) return; + tooltip.textContent = icon.dataset.hint || ""; + tooltip.style.display = "block"; + positionTooltip(e); + }, true); + + document.addEventListener("mouseleave", function (e) { + const icon = e.target.closest && e.target.closest(".hint-icon"); + if (!icon) return; + tooltip.style.display = "none"; + }, true); + + document.addEventListener("mousemove", function (e) { + if (tooltip.style.display === "block") positionTooltip(e); + }); + + document.addEventListener("click", function (e) { + const icon = e.target.closest && e.target.closest(".hint-icon"); + if (!icon) return; + e.preventDefault(); + tooltip.style.display = "none"; + ensureHintModal(); + document.getElementById("hint-modal-title").textContent = icon.dataset.hintTitle || "Hinweis"; + document.getElementById("hint-modal-body").textContent = icon.dataset.hint || ""; + openModal("hint-modal"); + }); + } + /* ---------------- Warnung bei ungespeicherten Änderungen ---------------- */ /* Erkennt generisch auf JEDER Seite, ob ein Formular mit echten Eingabefeldern (nicht nur versteckten Aktions-Feldern wie bei Löschen/ @@ -628,6 +724,7 @@ initLeaseCountdowns(); initUnsavedChangesGuard(); initSortableTables(); + initHintIcons(); document.querySelectorAll("[data-theme-toggle]").forEach((btn) => btn.addEventListener("click", toggleTheme)); }); diff --git a/srv/tesm/templates/_hint_icon.html b/srv/tesm/templates/_hint_icon.html new file mode 100644 index 0000000..b776835 --- /dev/null +++ b/srv/tesm/templates/_hint_icon.html @@ -0,0 +1,9 @@ +{# + Gemeinsames "i"-Hinweis-Icon: Hover zeigt den Text als Tooltip neben dem + Mauszeiger, Klick/Tap öffnet dasselbe (wiederverwendete) Modal mit dem + vollen Text -- Mechanik lebt zentral in app.js (initHintIcons), hier wird + nur das Icon mit seinen Daten-Attributen erzeugt. title = Kontext für die + Modal-Überschrift (z.B. das zugehörige Feld-Label), text = der eigentliche + Erklärungstext. +#} +{% macro hint_icon(text, title='Hinweis') %}{% endmacro %} diff --git a/srv/tesm/templates/credentials.html b/srv/tesm/templates/credentials.html index 20e86a3..ebe4c8d 100644 --- a/srv/tesm/templates/credentials.html +++ b/srv/tesm/templates/credentials.html @@ -8,6 +8,7 @@ {% block page_sub %}
{{ credentials|length }} Zugangsdaten
{% endblock %} {% block content %} +{% import "_hint_icon.html" as hi %}
@@ -95,13 +96,12 @@
-
+
-
Bestimmt u.a., ob diese Zugangsdaten unter „Wartung“ für Bulk-Updates nutzbar sind.
diff --git a/srv/tesm/templates/devices.html b/srv/tesm/templates/devices.html index f23f1f6..a03a46d 100644 --- a/srv/tesm/templates/devices.html +++ b/srv/tesm/templates/devices.html @@ -13,6 +13,7 @@ {% block page_sub %}
{{ devices|length }} Geräte
{% endblock %} {% block content %} +{% import "_hint_icon.html" as hi %}
@@ -125,9 +126,8 @@
Bitte eine gültige MAC-Adresse eingeben (xx:xx:xx:xx:xx:xx).
-
+
-
Physische Port-Nummer AM SWITCH (z.B. Port 3 von 48), an dem das Gerät eingesteckt ist — für den PoE-Neustart. Kein Netzwerk-/TCP-Port, keine SSH-Anmeldung.
SSH-Zugriff (optional, z.B. für Update-Aktionen unter „Wartung“)
-
+
-
TCP-Netzwerk-Port für die SSH-Anmeldung an der IP-Adresse dieses Geräts (siehe Feld „IP-Adresse“ oben). Leer lassen, wenn Standard-Port 22 verwendet wird.
@@ -201,14 +200,12 @@
Bitte eine gültige MAC-Adresse eingeben (xx:xx:xx:xx:xx:xx).
-
+
-
Portnummer am zugeordneten Switch, für den PoE-Neustart. Hat nichts mit dem SSH-Port unten zu tun.
SSH-Zugriff (optional, z.B. für Update-Aktionen unter „Wartung“)
-
+
-
TCP-Netzwerk-Port für die SSH-Anmeldung an der IP-Adresse dieses Geräts (siehe Feld „IP-Adresse“ oben). Leer lassen, wenn Standard-Port 22 verwendet wird.
diff --git a/srv/tesm/templates/fileshare.html b/srv/tesm/templates/fileshare.html index 947bf86..1b7c37d 100644 --- a/srv/tesm/templates/fileshare.html +++ b/srv/tesm/templates/fileshare.html @@ -4,6 +4,7 @@ {% block page_sub %}
{{ selected_share }}{% if rel_path %} / {{ rel_path }}{% endif %}
{% endblock %} {% block content %} +{% import "_hint_icon.html" as hi %} {% macro render_tree_node(share, path, name, expanded_map, active_path) %}
  • @@ -161,10 +162,9 @@
  • diff --git a/srv/tesm/templates/groups.html b/srv/tesm/templates/groups.html index 2ffde9b..40f5751 100644 --- a/srv/tesm/templates/groups.html +++ b/srv/tesm/templates/groups.html @@ -4,6 +4,7 @@ {% block page_sub %}
    {{ groups|length + 1 }} Gruppen · Rechteverwaltung
    {% endblock %} {% block content %} +{% import "_hint_icon.html" as hi %} {% macro permission_table(group, group_key, checked_keys, readonly) %} {% set row_types = group_row_types[group_key] %} @@ -69,11 +70,7 @@
    -

    Gruppen

    -
    - Rechte je Gruppe granular vergeben, Mitgliedschaft in mehreren Gruppen addiert sich. - „Admin“ und „Benutzer“ sind feste Systemgruppen. Legende direkt bei den Rechten. -
    +

    Gruppen {{ hi.hint_icon("Rechte je Gruppe granular vergeben, Mitgliedschaft in mehreren Gruppen addiert sich. „Admin“ und „Benutzer“ sind feste Systemgruppen. Legende direkt bei den Rechten.", "Gruppen") }}

    {% if current_user.has_permission('groups.create') %}
    - + -
    Standard: wöchentlich, 4 Rotationen (≈ 1 Monat Historie je Log).
    -
    Leeren Wert speichern, um eine Standard-Option wieder auszublenden.
    +
    + Standard-Option zurücksetzen {{ hi.hint_icon("Leeren Wert speichern, um eine Standard-Option wieder auszublenden.", "Standard-Option zurücksetzen") }} +
    {% else %} {{ option_values.get(d.id, {}).get('', '—') }} {% endif %} @@ -272,11 +266,7 @@
    -

    Aktive Leases

    -
    - 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. -
    +

    Aktive Leases {{ hi.hint_icon("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.", "Aktive Leases") }}

    @@ -497,9 +487,8 @@
    -
    +
    -
    Nur Kleinbuchstaben, Ziffern und Bindestrich, muss mit einem Buchstaben beginnen.
    -
    +
    -
    Muss in einem der oben konfigurierten Subnetze liegen, sonst wird sie beim Schreiben übersprungen.
    -
    +
    -
    Wird zum Hostname der Reservierung. DHCP-Optionen (z.B. Hostname erzwingen) lassen sich danach über das Options-Symbol in der Tabelle setzen.