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>
119 lines
5.4 KiB
HTML
119 lines
5.4 KiB
HTML
{% extends "base.html" %}
|
|
{% block page_title %}Mein Konto{% endblock %}
|
|
{% block page_sub %}<div class="topbar-sub">{{ current_user.username }}</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;">Profil</h2>
|
|
<div class="hint">Name und Profilbild ändern sich in der Sidebar und im Änderungslog.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex; align-items:center; gap:14px; margin-bottom:20px;">
|
|
{% if current_user.avatar_url %}
|
|
<img src="{{ current_user.avatar_url }}" alt="" style="width:56px; height:56px; border-radius:50%; object-fit:cover;">
|
|
{% else %}
|
|
<div class="user-avatar" style="width:56px; height:56px; font-size:18px;">{{ current_user.username[:2]|upper }}</div>
|
|
{% endif %}
|
|
<form method="post" action="{{ url_for('profile') }}" enctype="multipart/form-data" id="avatarForm" style="flex:1;">
|
|
<input type="file" name="avatar" id="avatarInput" accept="image/png,image/jpeg,image/gif,image/webp" style="display:none;" onchange="document.getElementById('avatarForm').requestSubmit();">
|
|
<button type="button" class="btn btn-secondary" style="width:100%;" onclick="document.getElementById('avatarInput').click();">Profilbild ändern</button>
|
|
<input type="hidden" name="upload_avatar" value="1">
|
|
</form>
|
|
</div>
|
|
|
|
<form method="post" action="{{ url_for('profile') }}" id="profileForm">
|
|
<div class="field"><label>Vorname</label><input type="text" name="first_name" value="{{ current_user.first_name or '' }}"></div>
|
|
<div class="field"><label>Name</label><input type="text" name="last_name" value="{{ current_user.last_name or '' }}"></div>
|
|
<div class="field"><label>Username</label><input type="text" value="{{ current_user.username }}" disabled></div>
|
|
<button type="submit" name="update_profile" value="1" class="btn btn-primary btn-block">Speichern</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Passwort ändern</h2>
|
|
<div class="hint">Erfordert Eingabe des aktuellen Passworts.</div>
|
|
</div>
|
|
</div>
|
|
<form method="post" action="{{ url_for('profile') }}" id="passwordForm">
|
|
<div class="field"><label>Aktuelles Passwort</label><input type="password" name="current_password" autocomplete="current-password"></div>
|
|
<div class="field"><label>Neues Passwort</label><input type="password" name="new_password" autocomplete="new-password"></div>
|
|
<div class="field"><label>Neues Passwort bestätigen</label><input type="password" name="confirm_password" autocomplete="new-password"></div>
|
|
<button type="submit" name="change_password" value="1" class="btn btn-secondary btn-block">Passwort ändern</button>
|
|
</form>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{% if current_user.is_admin %}
|
|
{% macro nav_order_buttons() %}
|
|
<div class="nav-order-actions">
|
|
<button type="button" class="icon-btn" title="Nach oben" onclick="moveNavItem(this,-1)">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
|
|
</button>
|
|
<button type="button" class="icon-btn" title="Nach unten" onclick="moveNavItem(this,1)">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg>
|
|
</button>
|
|
</div>
|
|
{% endmacro %}
|
|
<div class="card card-pad" style="margin-top:24px;">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Navbar-Reihenfolge</h2>
|
|
<div class="hint">Reihenfolge der Sidebar-Menüpunkte inkl. Unterpunkte.</div>
|
|
</div>
|
|
</div>
|
|
<form method="post" action="{{ url_for('save_nav_order') }}" id="navOrderForm">
|
|
<ul class="nav-order-list" id="navOrderList">
|
|
{% for item in full_nav_items %}
|
|
<li data-key="{{ item.key }}">
|
|
<div class="nav-order-row">
|
|
<span>{{ item.label }}</span>
|
|
{{ nav_order_buttons() }}
|
|
</div>
|
|
<input type="hidden" name="nav_order" value="{{ item.key }}">
|
|
{% if item.children %}
|
|
<ul class="nav-order-sublist">
|
|
{% for child in item.children %}
|
|
<li data-key="{{ child.key }}">
|
|
<div class="nav-order-row">
|
|
<span>{{ child.label }}</span>
|
|
{{ nav_order_buttons() }}
|
|
</div>
|
|
<input type="hidden" name="nav_child_order_{{ item.key }}" value="{{ child.key }}">
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<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>
|
|
Reihenfolge speichern
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function moveNavItem(btn, dir) {
|
|
const li = btn.closest("li");
|
|
const target = dir === -1 ? li.previousElementSibling : li.nextElementSibling;
|
|
if (!target) return;
|
|
if (dir === -1) li.parentNode.insertBefore(li, target);
|
|
else li.parentNode.insertBefore(target, li);
|
|
}
|
|
</script>
|
|
{% endblock %}
|