Files
tesm/srv/poe_manager/templates/base.html
T
alientim 255308f81b App-weite Warnung bei ungespeicherten Aenderungen + Options-UI-Fix + Cache-Busting
1. Generischer Ja/Nein-Dialog bei ungespeicherten Aenderungen (kein Opt-in
   pro Seite noetig): initUnsavedChangesGuard() erkennt automatisch jedes
   Formular mit echten Eingabefeldern, das seit dem Laden der Seite
   veraendert wurde, und fragt beim Klick auf einen Link (z.B. Sidebar-
   Navigation), ob zuerst gespeichert werden soll -- eigener Dialog
   (confirmSaveDiscard) statt des bestehenden confirmAction (dort ist
   "Abbrechen" = Bleiben, hier gibt es bewusst nur die zwei angefragten
   Optionen: speichern oder verwerfen, beide verlassen die Seite).
   "Ja, speichern" sendet das Formular ganz normal ab (echter POST) und
   merkt sich das eigentliche Ziel in sessionStorage, um danach automatisch
   dorthin weiterzuleiten.

   Nachtrag noch in dieser Session gefixt: ein normales Speichern ueber den
   Seiten-eigenen Button loeste zusaetzlich die native Browser-Warnung
   (beforeunload) aus, obwohl die Aenderung bereits gespeichert wurde --
   navigatingAway-Flag wird jetzt bei jedem Submit eines beobachteten
   Formulars (eigener Button ODER eigener Ja/Nein-Dialog) gesetzt, die
   generische Browser-Warnung erscheint dadurch nur noch bei tatsaechlich
   ungefragter Navigation (Tab schliessen, Reload, Adresszeile). Live per
   Playwright in drei Szenarien verifiziert (direktes Speichern, Ja via
   eigenem Dialog, Nein via eigenem Dialog) -- kein Browser-Popup in den
   ersten beiden Faellen, korrektes Redirect zum urspruenglichen Ziel nach
   "Ja".

2. DHCP-Options-Dropdown (Reservierungs-Dialog) verschiebt ein neu
   ausgewaehltes Options-Feld jetzt direkt unter das Dropdown (in einen
   .poe-added-options-Container) statt es nur an seiner urspruenglichen
   Stelle in der bis zu 57 Eintraege langen Liste einzublenden -- lag dort
   oft ausserhalb des sichtbaren Modal-Ausschnitts und wirkte, als waere
   nichts passiert. Bereits gesetzte Overrides stehen von Anfang an im
   selben Container, weitere haengen sich beim Auswaehlen unten an.

3. Cache-Busting fuer app.js/style.css (asset_url()-Helper, haengt die
   Aenderungszeit der Datei als ?v=... an) -- ein im Browser gecachtes,
   veraltetes app.js konnte nach einem Update sonst den Eindruck erwecken,
   eine neue Funktion wuerde "nicht funktionieren", obwohl der Code auf dem
   Server laengst aktuell war.

Alle drei live auf der Test-VM deployt und per Playwright verifiziert,
keine Fehler im journalctl-Log. (Testartefakt aufgeraeumt: der echte
Host-Hostname wurde durch die Playwright-Tests versehentlich auf
"directsave-test"/"navsave-test" geaendert und wieder auf POETEST
zurueckgesetzt.)
2026-08-11 16:39:03 +02:00

154 lines
8.8 KiB
HTML

<!DOCTYPE html>
<html lang="de" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title or "PoE Manager" }}</title>
<link rel="icon" href="{{ url_for('static', filename='images/logo.png') }}">
<link rel="stylesheet" href="{{ asset_url('css/style.css') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
{% set icons = {
"grid": '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>',
"cpu": '<rect x="6" y="6" width="12" height="12" rx="1.5"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/>',
"share": '<circle cx="18" cy="5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="19" r="2.5"/><path d="M8.2 10.7l7.6-4.4M8.2 13.3l7.6 4.4"/>',
"users": '<circle cx="9" cy="8" r="3.2"/><path d="M2.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6"/><circle cx="17.5" cy="8.5" r="2.4"/><path d="M15.8 14.2c2.7.3 4.7 2.4 4.7 5.3"/>',
"groups": '<rect x="3" y="4" width="8" height="7" rx="1.5"/><rect x="13" y="4" width="8" height="7" rx="1.5"/><rect x="3" y="13" width="8" height="7" rx="1.5"/><rect x="13" y="13" width="8" height="7" rx="1.5"/>',
"key": '<circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M17 6l3 3M14 9l2 2"/>',
"terminal": '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 9l4 3-4 3M13 15h5"/>',
"history": '<path d="M3 12a9 9 0 109-9 9.75 9.75 0 00-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
"sliders": '<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"/>',
"logout": '<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/>',
"gear": '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06A1.65 1.65 0 004.6 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>',
"transfer": '<path d="M17 3l4 4-4 4"/><path d="M3 7h18"/><path d="M7 21l-4-4 4-4"/><path d="M21 17H3"/>',
"network": '<rect x="9" y="2" width="6" height="6" rx="1.2"/><rect x="2" y="16" width="6" height="6" rx="1.2"/><rect x="16" y="16" width="6" height="6" rx="1.2"/><path d="M12 8v4M12 12H5v4M12 12h7v4"/>',
} %}
<div class="app-shell">
{% if current_user.is_authenticated %}
<div class="sidebar-backdrop" data-sidebar-toggle></div>
<aside class="sidebar">
<div class="sidebar-brand">
<div>
<span class="brand-name">PoE Manager</span>
<span class="brand-sub">Device Monitoring</span>
</div>
</div>
<nav class="sidebar-nav">
{% for item in nav_items_ordered %}
{% if item.children %}
{% set child_active = item.children|selectattr('endpoint', 'equalto', request.endpoint)|list %}
<div class="nav-group {% if child_active %}expanded active-group{% endif %}" data-nav-group data-nav-group-key="{{ item.key }}">
<button type="button" class="nav-group-toggle" data-nav-group-toggle>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ icons[item.icon]|safe }}</svg>
{{ item.label }}
<svg class="nav-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
</button>
<div class="nav-group-children">
{% for child in item.children %}
<a href="{{ url_for(child.endpoint) }}" class="nav-item {% if request.endpoint == child.endpoint %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ icons[child.icon]|safe }}</svg>
{{ child.label }}
</a>
{% endfor %}
</div>
</div>
{% else %}
<a href="{{ url_for(item.endpoint) }}" class="nav-item {% if active_page == item.key %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ icons[item.icon]|safe }}</svg>
{{ item.label }}
</a>
{% endif %}
{% endfor %}
</nav>
<div class="sidebar-footer">
<div class="user-chip">
{% if current_user.avatar_url %}
<img class="user-avatar" src="{{ current_user.avatar_url }}" alt="" style="object-fit:cover;">
{% else %}
<div class="user-avatar">{{ current_user.username[:2]|upper }}</div>
{% endif %}
<div class="user-meta">
<div class="u-name">{{ current_user.display_name }}</div>
<div class="u-role">{{ "Administrator" if current_user.is_admin else current_user.group_names or "Benutzer" }}</div>
</div>
<a href="{{ url_for('account') }}" class="icon-btn" title="Einstellungen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ icons['gear']|safe }}</svg>
</a>
</div>
<div class="footer-actions">
<button type="button" class="icon-btn" data-theme-toggle title="Theme wechseln">
<span data-theme-icon></span>
</button>
<a href="{{ url_for('logout') }}" class="icon-btn" title="Abmelden" style="flex:1; display:flex;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin:auto;">{{ icons['logout']|safe }}</svg>
</a>
</div>
</div>
</aside>
{% endif %}
<div class="main {% if not current_user.is_authenticated %}no-sidebar{% endif %}">
<div class="topbar">
<div class="topbar-left">
{% if current_user.is_authenticated %}
<button type="button" class="hamburger" data-sidebar-toggle aria-label="Menü">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
{% endif %}
<div>
<div class="topbar-title">{% block page_title %}Dashboard{% endblock %}</div>
{% block page_sub %}{% endblock %}
</div>
</div>
<div class="topbar-logo">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="WiS">
</div>
<div class="topbar-right">
<span class="timer-pill" id="global-timer-pill"
data-last-run-ms="{{ global_last_run_epoch_ms or '' }}"
data-interval-ms="{{ (global_check_interval * 60000) if global_check_interval else '' }}">
<span class="dot"></span>Nächste Prüfung in <span id="global-timer">--</span>s
{% if current_user.is_admin %}
<button type="button" class="timer-pill-refresh" id="run-check-now" title="Jetzt prüfen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3.2-6.9M21 4v5h-5"/></svg>
</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>
Login
</a>
{% endif %}
</div>
</div>
<div class="content">
{% block content %}{% endblock %}
</div>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
<script type="application/json" id="flashed-data">{{ messages|tojson }}</script>
{% endwith %}
<script src="{{ asset_url('js/app.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>