Live-Log-Fix, Sidebar-Untermenüs für Geräte/Einstellungen, eigenes Konto
- Live-Log-Bug behoben: _latest_log_file() sortierte nach Datei-ctime statt
nach Dateiname — auf manchen Dateisystemen (u.a. unter WSL2 auf einem
gemounteten Windows-Laufwerk) unzuverlässig und lieferte nicht immer das
tatsächlich neueste Logfile. Sortiert jetzt wie der Rest des Codes über
den chronologisch sortierbaren Dateinamen (rpi-YYYYMMDDHHMMSS.log).
- Navbar umstrukturiert in aufklappbare Gruppen mit Unterpunkten:
- "Geräte": Clients (bisherige Devices-Seite), Switche, Zugangsdaten
- "Einstellungen": Benutzer, Gruppen, Systemeinstellungen (Prüfintervall),
Im-/Export
- "Logs": Live, Änderungen (vormals Live-Log/Änderungslog)
Sichtbarkeit gilt jetzt auch pro Unterpunkt: eine Gruppe erscheint nur,
wenn mindestens ein Unterpunkt für den Benutzer sichtbar ist, und zeigt
dann auch nur die sichtbaren Unterpunkte.
- Im-/Export als eigene Unterseite mit Export/Import nebeneinander
(.settings-grid).
- Neue Seite "Mein Konto" (/account, erreichbar über ein Zahnrad-Symbol
neben dem eigenen Namen in der Sidebar) ersetzt das bisherige Profil-Modal:
Profil/Passwort/Profilbild als volle Seite, dazu für Admins die
Navbar-Reihenfolge (aus den Systemeinstellungen hierher verschoben).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,16 +44,16 @@ Die App ermöglicht:
|
|||||||
zuletzt geändert hat. PoE-Neustarts werden bewusst **nicht** geloggt
|
zuletzt geändert hat. PoE-Neustarts werden bewusst **nicht** geloggt
|
||||||
(siehe [Änderungslog](#änderungslog))
|
(siehe [Änderungslog](#änderungslog))
|
||||||
- **Verschlüsselter Import/Export**: Geräte, Switche und Zugangsdaten lassen
|
- **Verschlüsselter Import/Export**: Geräte, Switche und Zugangsdaten lassen
|
||||||
sich unter Einstellungen als ein mit selbstgewählter Passphrase
|
sich unter „Einstellungen → Im-/Export" als ein mit selbstgewählter
|
||||||
verschlüsseltes Bundle exportieren und auf einer neuen Umgebung wieder
|
Passphrase verschlüsseltes Bundle exportieren und auf einer neuen Umgebung
|
||||||
importieren (siehe [Import/Export](#importexport))
|
wieder importieren (siehe [Import/Export](#importexport))
|
||||||
- **Eigenes Profil**: jeder Benutzer kann über einen Klick auf seinen Namen
|
- **Eigenes Konto**: das Zahnrad-Symbol neben dem eigenen Namen unten in der
|
||||||
in der Sidebar Vor-/Nachname, eigenes Passwort und ein Profilbild pflegen
|
Sidebar führt zu „Mein Konto“ — Vor-/Nachname, eigenes Passwort und
|
||||||
(siehe [Eigenes Profil](#eigenes-profil--profilbild))
|
Profilbild pflegen (siehe [Eigenes Konto](#eigenes-konto--profilbild))
|
||||||
- **Anpassbare Navigation**: Admins können die Reihenfolge der Sidebar-
|
- **Anpassbare Navigation**: Admins legen dort zusätzlich die Reihenfolge der
|
||||||
Menüpunkte unter Einstellungen per Auf/Ab-Buttons festlegen; jeder Benutzer
|
Sidebar-Hauptpunkte per Auf-/Ab-Buttons fest; jeder Benutzer sieht davon
|
||||||
sieht davon ohnehin nur, wofür er berechtigt ist (siehe
|
ohnehin nur, wofür er berechtigt ist — auch pro Unterpunkt innerhalb einer
|
||||||
[Navigation anpassen](#navigation-anpassen))
|
Gruppe (siehe [Navigation anpassen](#navigation-anpassen))
|
||||||
|
|
||||||
## Was ist neu gegenüber dem Original?
|
## Was ist neu gegenüber dem Original?
|
||||||
|
|
||||||
@@ -185,24 +185,35 @@ angelegt. Switche referenzieren ihre Zugangsdaten dabei über den Namen (nicht
|
|||||||
die interne ID), damit der Import auch bei abweichender ID-Vergabe auf dem
|
die interne ID), damit der Import auch bei abweichender ID-Vergabe auf dem
|
||||||
Zielsystem korrekt verknüpft.
|
Zielsystem korrekt verknüpft.
|
||||||
|
|
||||||
## Eigenes Profil & Profilbild
|
## Eigenes Konto & Profilbild
|
||||||
|
|
||||||
Ein Klick auf den eigenen Namen unten in der Sidebar öffnet „Mein Profil“:
|
Das Zahnrad-Symbol neben dem eigenen Namen unten in der Sidebar führt zu
|
||||||
Vor-/Nachname ändern, ein Profilbild hochladen (PNG/JPG/GIF/WEBP) und das
|
**„Mein Konto“** (`/account`): Vor-/Nachname ändern, ein Profilbild
|
||||||
eigene Passwort ändern (erfordert Eingabe des aktuellen Passworts). Das ist
|
hochladen (PNG/JPG/GIF/WEBP) und das eigene Passwort ändern (erfordert
|
||||||
bewusst getrennt vom Admin-Bereich „Benutzer“, der andere Konten verwaltet.
|
Eingabe des aktuellen Passworts). Das ist bewusst getrennt vom Admin-Bereich
|
||||||
Ist ein Profilbild hinterlegt, erscheint es statt der Initialen sowohl in
|
„Benutzer“, der andere Konten verwaltet. Ist ein Profilbild hinterlegt,
|
||||||
der Sidebar als auch im Änderungslog neben dem Benutzernamen.
|
erscheint es statt der Initialen sowohl in der Sidebar als auch im
|
||||||
|
Änderungslog neben dem Benutzernamen. Admins sehen auf derselben Seite
|
||||||
|
zusätzlich die Navbar-Reihenfolge (siehe unten).
|
||||||
|
|
||||||
## Navigation anpassen
|
## Navigation anpassen
|
||||||
|
|
||||||
Die Reihenfolge der Sidebar-Menüpunkte ist unter **Einstellungen** per
|
Die Sidebar ist in drei aufklappbare Gruppen strukturiert:
|
||||||
Auf-/Ab-Buttons konfigurierbar (gespeichert als JSON-Liste unter
|
|
||||||
`settings.nav_order`) und gilt global für alle Benutzer. Unabhängig von der
|
- **Geräte** — Clients (die eigentlichen Devices), Switche, Zugangsdaten
|
||||||
Reihenfolge sieht jeder Benutzer ohnehin nur die Punkte, für die er auch
|
- **Einstellungen** — Benutzer, Gruppen, Systemeinstellungen (Prüfintervall),
|
||||||
berechtigt ist — ein Benutzer ohne jede Gruppe sieht z.B. nur „Dashboard“.
|
Im-/Export
|
||||||
„Logs“ ist dabei eine aufklappbare Gruppe mit den Unterpunkten „Live-Log“
|
- **Logs** — Live, Änderungen (Änderungslog)
|
||||||
und „Änderungslog“.
|
|
||||||
|
Die Reihenfolge dieser drei Gruppen (und von „Dashboard“) ist für Admins
|
||||||
|
unter **Mein Konto** (Zahnrad-Symbol in der Sidebar) per Auf-/Ab-Buttons
|
||||||
|
konfigurierbar (gespeichert als JSON-Liste unter `settings.nav_order`) und
|
||||||
|
gilt global für alle Benutzer. Unabhängig von der Reihenfolge sieht jeder
|
||||||
|
Benutzer ohnehin nur, wofür er berechtigt ist — sowohl auf Gruppenebene als
|
||||||
|
auch pro Unterpunkt: hat ein Benutzer z.B. nur `devices.view`, erscheint die
|
||||||
|
Gruppe „Geräte“ nur mit dem Unterpunkt „Clients“ (ohne Switche/
|
||||||
|
Zugangsdaten); eine Gruppe verschwindet komplett, wenn kein Unterpunkt
|
||||||
|
sichtbar ist.
|
||||||
|
|
||||||
## Entwicklung unter WSL
|
## Entwicklung unter WSL
|
||||||
|
|
||||||
|
|||||||
+64
-26
@@ -150,36 +150,43 @@ DEFAULT_GROUP_NAME = "Benutzer"
|
|||||||
DEFAULT_GROUP_PERMISSIONS = ["devices.view", "switches.view"]
|
DEFAULT_GROUP_PERMISSIONS = ["devices.view", "switches.view"]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Navbar — Reihenfolge ist admin-konfigurierbar (siehe settings(), gespeichert
|
# Navbar — die Reihenfolge der TOP-LEVEL-Punkte ist admin-konfigurierbar
|
||||||
# als JSON-Liste von Keys unter settings.nav_order); standardmäßig wird pro
|
# (siehe /account, gespeichert als JSON-Liste von Keys unter
|
||||||
# Benutzer nur angezeigt, wofür er auch tatsächlich eine Berechtigung hat.
|
# settings.nav_order); standardmäßig wird pro Benutzer nur angezeigt, wofür
|
||||||
# "logs" ist eine Gruppe mit Untermenü (Live-Log + Änderungslog).
|
# er auch tatsächlich eine Berechtigung hat — das gilt auch pro Unterpunkt
|
||||||
|
# innerhalb einer Gruppe: eine Gruppe erscheint nur, wenn mindestens ein
|
||||||
|
# Unterpunkt sichtbar ist, und zeigt dann auch nur die sichtbaren Unterpunkte.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
NAV_ITEMS = [
|
NAV_ITEMS = [
|
||||||
{"key": "index", "label": "Dashboard", "icon": "grid", "endpoint": "index"},
|
{"key": "index", "label": "Dashboard", "icon": "grid", "endpoint": "index"},
|
||||||
{"key": "devices", "label": "Geräte", "icon": "cpu", "endpoint": "devices"},
|
{"key": "devices_group", "label": "Geräte", "icon": "cpu", "children": [
|
||||||
{"key": "switches", "label": "Switche", "icon": "share", "endpoint": "switches"},
|
{"key": "devices", "label": "Clients", "icon": "cpu", "endpoint": "devices"},
|
||||||
{"key": "credentials", "label": "Zugangsdaten", "icon": "key", "endpoint": "credentials"},
|
{"key": "switches", "label": "Switche", "icon": "share", "endpoint": "switches"},
|
||||||
{"key": "users", "label": "Benutzer", "icon": "users", "endpoint": "users"},
|
{"key": "credentials", "label": "Zugangsdaten", "icon": "key", "endpoint": "credentials"},
|
||||||
{"key": "groups", "label": "Gruppen", "icon": "groups", "endpoint": "groups"},
|
]},
|
||||||
{"key": "logs", "label": "Logs", "icon": "terminal", "children": [
|
{"key": "settings_group", "label": "Einstellungen", "icon": "sliders", "children": [
|
||||||
{"key": "logs_live", "label": "Live-Log", "icon": "terminal", "endpoint": "logs"},
|
{"key": "users", "label": "Benutzer", "icon": "users", "endpoint": "users"},
|
||||||
{"key": "logs_activity", "label": "Änderungslog", "icon": "history", "endpoint": "activity_log"},
|
{"key": "groups", "label": "Gruppen", "icon": "groups", "endpoint": "groups"},
|
||||||
|
{"key": "settings_system", "label": "Systemeinstellungen", "icon": "sliders", "endpoint": "settings"},
|
||||||
|
{"key": "settings_importexport", "label": "Im-/Export", "icon": "transfer", "endpoint": "settings_import_export"},
|
||||||
|
]},
|
||||||
|
{"key": "logs_group", "label": "Logs", "icon": "terminal", "children": [
|
||||||
|
{"key": "logs_live", "label": "Live", "icon": "terminal", "endpoint": "logs"},
|
||||||
|
{"key": "logs_activity", "label": "Änderungen", "icon": "history", "endpoint": "activity_log"},
|
||||||
]},
|
]},
|
||||||
{"key": "settings", "label": "Einstellungen", "icon": "sliders", "endpoint": "settings"},
|
|
||||||
]
|
]
|
||||||
DEFAULT_NAV_ORDER = [item["key"] for item in NAV_ITEMS]
|
DEFAULT_NAV_ORDER = [item["key"] for item in NAV_ITEMS]
|
||||||
NAV_ITEMS_BY_KEY = {item["key"]: item for item in NAV_ITEMS}
|
NAV_ITEMS_BY_KEY = {item["key"]: item for item in NAV_ITEMS}
|
||||||
|
|
||||||
|
|
||||||
def _nav_item_visible(key, user):
|
def _nav_key_visible(key, user):
|
||||||
if key == "index":
|
if key == "index":
|
||||||
return True
|
return True
|
||||||
if key == "devices":
|
if key == "devices":
|
||||||
return user.can_view_devices
|
return user.can_view_devices
|
||||||
if key in ("switches", "credentials"):
|
if key in ("switches", "credentials"):
|
||||||
return user.can_manage_switches
|
return user.can_manage_switches
|
||||||
if key in ("users", "groups", "logs", "settings"):
|
if key in ("users", "groups", "settings_system", "settings_importexport", "logs_live", "logs_activity"):
|
||||||
return user.is_admin
|
return user.is_admin
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -194,7 +201,16 @@ def inject_nav():
|
|||||||
stored_order = []
|
stored_order = []
|
||||||
ordered_keys = [k for k in stored_order if k in NAV_ITEMS_BY_KEY]
|
ordered_keys = [k for k in stored_order if k in NAV_ITEMS_BY_KEY]
|
||||||
ordered_keys += [k for k in DEFAULT_NAV_ORDER if k not in ordered_keys]
|
ordered_keys += [k for k in DEFAULT_NAV_ORDER if k not in ordered_keys]
|
||||||
nav_items_ordered = [NAV_ITEMS_BY_KEY[k] for k in ordered_keys if _nav_item_visible(k, current_user)]
|
|
||||||
|
nav_items_ordered = []
|
||||||
|
for k in ordered_keys:
|
||||||
|
item = NAV_ITEMS_BY_KEY[k]
|
||||||
|
if item.get("children"):
|
||||||
|
visible_children = [c for c in item["children"] if _nav_key_visible(c["key"], current_user)]
|
||||||
|
if visible_children:
|
||||||
|
nav_items_ordered.append({**item, "children": visible_children})
|
||||||
|
elif _nav_key_visible(k, current_user):
|
||||||
|
nav_items_ordered.append(item)
|
||||||
return {"nav_items_ordered": nav_items_ordered}
|
return {"nav_items_ordered": nav_items_ordered}
|
||||||
|
|
||||||
|
|
||||||
@@ -507,12 +523,19 @@ def logout():
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Eigenes Profil — jeder eingeloggte Benutzer darf Vor-/Nachname, eigenes
|
# Eigenes Profil / Konto — jeder eingeloggte Benutzer darf Vor-/Nachname,
|
||||||
# Passwort und Profilbild selbst ändern (Klick auf den eigenen Namen in der
|
# eigenes Passwort und Profilbild selbst ändern (Zahnrad-Symbol neben dem
|
||||||
# Sidebar). Bewusst getrennt vom Admin-"Users"-Bereich, der andere Benutzer
|
# eigenen Namen unten in der Sidebar). Bewusst getrennt vom Admin-"Users"-
|
||||||
# verwaltet.
|
# Bereich, der andere Benutzer verwaltet. Admins konfigurieren hier
|
||||||
|
# zusätzlich die Navbar-Reihenfolge (siehe save_nav_order()).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@app.route("/account")
|
||||||
|
@login_required
|
||||||
|
def account():
|
||||||
|
return render_template("account.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/profile", methods=["POST"])
|
@app.route("/profile", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def profile():
|
def profile():
|
||||||
@@ -576,10 +599,16 @@ def profile():
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _latest_log_file():
|
def _latest_log_file():
|
||||||
|
"""Neuestes Logfile ermitteln. Bewusst über den Dateinamen sortiert
|
||||||
|
(rpi-YYYYMMDDHHMMSS.log ist lexikografisch = chronologisch sortierbar),
|
||||||
|
NICHT über Dateisystem-Metadaten wie ctime/mtime — die sind je nach
|
||||||
|
Dateisystem (z.B. unter WSL2 auf einem gemounteten Windows-Laufwerk)
|
||||||
|
unzuverlässig und haben in der Praxis nicht immer das tatsächlich
|
||||||
|
zuletzt geschriebene Logfile geliefert."""
|
||||||
log_files = glob.glob(LOG_GLOB)
|
log_files = glob.glob(LOG_GLOB)
|
||||||
if not log_files:
|
if not log_files:
|
||||||
return None
|
return None
|
||||||
return max(log_files, key=os.path.getctime)
|
return max(log_files)
|
||||||
|
|
||||||
|
|
||||||
def get_last_seen(dev_name: str):
|
def get_last_seen(dev_name: str):
|
||||||
@@ -758,6 +787,15 @@ def settings():
|
|||||||
return render_template("settings.html", interval=interval)
|
return render_template("settings.html", interval=interval)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/settings/import-export")
|
||||||
|
@login_required
|
||||||
|
def settings_import_export():
|
||||||
|
if not current_user.is_admin:
|
||||||
|
flash("Nur Admins dürfen Daten importieren/exportieren!", "danger")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
return render_template("settings_import_export.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/settings/nav-order", methods=["POST"])
|
@app.route("/settings/nav-order", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def save_nav_order():
|
def save_nav_order():
|
||||||
@@ -770,7 +808,7 @@ def save_nav_order():
|
|||||||
set_setting("nav_order", json.dumps(order))
|
set_setting("nav_order", json.dumps(order))
|
||||||
log_action("settings.update", "Navbar-Reihenfolge")
|
log_action("settings.update", "Navbar-Reihenfolge")
|
||||||
flash("Navbar-Reihenfolge gespeichert.", "success")
|
flash("Navbar-Reihenfolge gespeichert.", "success")
|
||||||
return redirect(url_for("settings"))
|
return redirect(url_for("account"))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -791,7 +829,7 @@ def export_data():
|
|||||||
passphrase = request.form.get("export_passphrase", "")
|
passphrase = request.form.get("export_passphrase", "")
|
||||||
if not passphrase:
|
if not passphrase:
|
||||||
flash("Bitte eine Passphrase für den Export angeben.", "danger")
|
flash("Bitte eine Passphrase für den Export angeben.", "danger")
|
||||||
return redirect(url_for("settings"))
|
return redirect(url_for("settings_import_export"))
|
||||||
|
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
credentials_rows = conn.execute("SELECT name, username, password FROM credentials").fetchall()
|
credentials_rows = conn.execute("SELECT name, username, password FROM credentials").fetchall()
|
||||||
@@ -850,7 +888,7 @@ def import_data():
|
|||||||
upload = request.files.get("import_file")
|
upload = request.files.get("import_file")
|
||||||
if not passphrase or not upload or not upload.filename:
|
if not passphrase or not upload or not upload.filename:
|
||||||
flash("Bitte Passphrase und Export-Datei angeben.", "danger")
|
flash("Bitte Passphrase und Export-Datei angeben.", "danger")
|
||||||
return redirect(url_for("settings"))
|
return redirect(url_for("settings_import_export"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
envelope = json.loads(upload.read().decode("utf-8"))
|
envelope = json.loads(upload.read().decode("utf-8"))
|
||||||
@@ -859,7 +897,7 @@ def import_data():
|
|||||||
payload = json.loads(export_fernet.decrypt(envelope["payload"].encode("utf-8")).decode("utf-8"))
|
payload = json.loads(export_fernet.decrypt(envelope["payload"].encode("utf-8")).decode("utf-8"))
|
||||||
except Exception:
|
except Exception:
|
||||||
flash("Import fehlgeschlagen: Datei ungültig oder Passphrase falsch.", "danger")
|
flash("Import fehlgeschlagen: Datei ungültig oder Passphrase falsch.", "danger")
|
||||||
return redirect(url_for("settings"))
|
return redirect(url_for("settings_import_export"))
|
||||||
|
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
n_cred = n_switch = n_dev = 0
|
n_cred = n_switch = n_dev = 0
|
||||||
@@ -898,7 +936,7 @@ def import_data():
|
|||||||
|
|
||||||
log_action("data.import", "Devices/Switches/Zugangsdaten", f"{n_dev} Geräte, {n_switch} Switche, {n_cred} Zugangsdaten")
|
log_action("data.import", "Devices/Switches/Zugangsdaten", f"{n_dev} Geräte, {n_switch} Switche, {n_cred} Zugangsdaten")
|
||||||
flash(f"Import abgeschlossen: {n_cred} Zugangsdaten, {n_switch} Switche, {n_dev} Geräte.", "success")
|
flash(f"Import abgeschlossen: {n_cred} Zugangsdaten, {n_switch} Switche, {n_dev} Geräte.", "success")
|
||||||
return redirect(url_for("settings"))
|
return redirect(url_for("settings_import_export"))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -200,6 +200,14 @@ button { font-family: inherit; }
|
|||||||
.nav-group.expanded .nav-group-children { display: flex; }
|
.nav-group.expanded .nav-group-children { display: flex; }
|
||||||
.nav-group-children .nav-item { font-size: 13px; padding: 8px 12px; }
|
.nav-group-children .nav-item { font-size: 13px; padding: 8px 12px; }
|
||||||
|
|
||||||
|
/* Kacheln nebeneinander (z.B. Im-/Export, Konto-Seite) */
|
||||||
|
.settings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
/* Navbar-Reihenfolge (Settings) */
|
/* Navbar-Reihenfolge (Settings) */
|
||||||
.nav-order-list {
|
.nav-order-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -247,7 +255,9 @@ button { font-family: inherit; }
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-meta { overflow: hidden; }
|
.user-meta { flex: 1; min-width: 0; overflow: hidden; }
|
||||||
|
.user-chip .icon-btn { flex-shrink: 0; width: 30px; height: 30px; }
|
||||||
|
.user-chip .icon-btn svg { width: 15px; height: 15px; }
|
||||||
.user-meta .u-name { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.user-meta .u-name { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.user-meta .u-role { font-size: 11px; color: var(--text-faint); }
|
.user-meta .u-role { font-size: 11px; color: var(--text-faint); }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block page_title %}Mein Konto{% endblock %}
|
||||||
|
{% block page_sub %}<div class="topbar-sub">{{ current_user.username }}</div>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<div class="card card-pad" style="margin-bottom:24px;">
|
||||||
|
<div class="section-head" style="margin-bottom:16px;">
|
||||||
|
<div>
|
||||||
|
<h2 style="font-size:16px;">Navbar-Reihenfolge</h2>
|
||||||
|
<div class="hint">Bestimmt, in welcher Reihenfolge die Menüpunkte in der Sidebar erscheinen. Jeder Benutzer sieht davon nur, wofür er auch berechtigt ist.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url_for('save_nav_order') }}" id="navOrderForm">
|
||||||
|
<ul class="nav-order-list" id="navOrderList">
|
||||||
|
{% for item in nav_items_ordered %}
|
||||||
|
<li data-key="{{ item.key }}">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<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>
|
||||||
|
<input type="hidden" name="nav_order" value="{{ item.key }}">
|
||||||
|
</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 %}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
{% 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 %}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set active_page = "logs" %}
|
{% set active_page = "logs" %}
|
||||||
{% block page_title %}Änderungslog{% endblock %}
|
{% block page_title %}Änderungen{% endblock %}
|
||||||
{% block page_sub %}<div class="topbar-sub">{{ entries|length }} Einträge (letzte 500)</div>{% endblock %}
|
{% block page_sub %}<div class="topbar-sub">{{ entries|length }} Einträge (letzte 500)</div>{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
"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"/>',
|
"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"/>',
|
"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"/>',
|
"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"/>',
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
@@ -65,7 +67,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<button type="button" class="user-chip" data-open-modal="profileModal" onclick="resetProfileForms();" style="cursor:pointer; text-align:left; background:none; border:none; width:100%;">
|
<div class="user-chip">
|
||||||
{% if current_user.avatar_url %}
|
{% if current_user.avatar_url %}
|
||||||
<img class="user-avatar" src="{{ current_user.avatar_url }}" alt="" style="object-fit:cover;">
|
<img class="user-avatar" src="{{ current_user.avatar_url }}" alt="" style="object-fit:cover;">
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -75,7 +77,10 @@
|
|||||||
<div class="u-name">{{ current_user.display_name }}</div>
|
<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 class="u-role">{{ "Administrator" if current_user.is_admin else current_user.group_names or "Benutzer" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
<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">
|
<div class="footer-actions">
|
||||||
<button type="button" class="icon-btn" data-theme-toggle title="Theme wechseln">
|
<button type="button" class="icon-btn" data-theme-toggle title="Theme wechseln">
|
||||||
<span data-theme-icon></span>
|
<span data-theme-icon></span>
|
||||||
@@ -86,49 +91,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Modal: Mein Profil (Klick auf eigenen Namen in der Sidebar) -->
|
|
||||||
<div class="modal-overlay" id="profileModal">
|
|
||||||
<div class="modal" style="max-width:440px;">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Mein Profil</h3>
|
|
||||||
<button type="button" class="modal-close" data-close-modal>×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<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 style="border-top:1px solid var(--border); margin:20px 0;"></div>
|
|
||||||
|
|
||||||
<form method="post" action="{{ url_for('profile') }}" id="passwordForm">
|
|
||||||
<h4 style="font-size:13px; text-transform:uppercase; letter-spacing:.04em; color:var(--text-dim); margin-bottom:12px;">Passwort ändern</h4>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="main {% if not current_user.is_authenticated %}no-sidebar{% endif %}">
|
<div class="main {% if not current_user.is_authenticated %}no-sidebar{% endif %}">
|
||||||
@@ -171,12 +133,6 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||||
<script>
|
|
||||||
function resetProfileForms() {
|
|
||||||
const pwForm = document.getElementById("passwordForm");
|
|
||||||
if (pwForm) pwForm.reset();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% set can_delete = current_user.has_permission('devices.delete') %}
|
{% set can_delete = current_user.has_permission('devices.delete') %}
|
||||||
{% set show_actions_col = can_edit or can_delete %}
|
{% set show_actions_col = can_edit or can_delete %}
|
||||||
{% set col_count = 5 + (1 if can_toggle else 0) + (1 if show_actions_col else 0) %}
|
{% set col_count = 5 + (1 if can_toggle else 0) + (1 if show_actions_col else 0) %}
|
||||||
{% block page_title %}Geräte{% endblock %}
|
{% block page_title %}Clients{% endblock %}
|
||||||
{% block page_sub %}<div class="topbar-sub">{{ devices|length }} Geräte</div>{% endblock %}
|
{% block page_sub %}<div class="topbar-sub">{{ devices|length }} Geräte</div>{% endblock %}
|
||||||
{% block topbar_right %}
|
{% block topbar_right %}
|
||||||
{% if can_create %}
|
{% if can_create %}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set active_page = "logs" %}
|
{% set active_page = "logs" %}
|
||||||
{% block page_title %}Live-Log{% endblock %}
|
{% block page_title %}Live{% endblock %}
|
||||||
{% block page_sub %}<div class="topbar-sub">{{ log_name or "kein Logfile" }}</div>{% endblock %}
|
{% block page_sub %}<div class="topbar-sub">{{ log_name or "kein Logfile" }}</div>{% endblock %}
|
||||||
{% block topbar_right %}
|
{% block topbar_right %}
|
||||||
<span class="timer-pill"><span class="dot"></span>Update in <span id="timer">--</span>s</span>
|
<span class="timer-pill"><span class="dot"></span>Update in <span id="timer">--</span>s</span>
|
||||||
|
|||||||
@@ -1,41 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set active_page = "settings" %}
|
{% set active_page = "settings_system" %}
|
||||||
{% block page_title %}Einstellungen{% endblock %}
|
{% block page_title %}Systemeinstellungen{% endblock %}
|
||||||
{% block page_sub %}<div class="topbar-sub">Prüfintervall für das Monitoring</div>{% endblock %}
|
{% block page_sub %}<div class="topbar-sub">Prüfintervall für das Monitoring</div>{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card card-pad" style="max-width:420px;">
|
<div class="card card-pad" style="max-width:420px;">
|
||||||
<div class="section-head" style="margin-bottom:16px;">
|
|
||||||
<div>
|
|
||||||
<h2 style="font-size:16px;">Navbar-Reihenfolge</h2>
|
|
||||||
<div class="hint">Bestimmt, in welcher Reihenfolge die Menüpunkte in der Sidebar erscheinen. Jeder Benutzer sieht davon nur, wofür er auch berechtigt ist.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form method="post" action="{{ url_for('save_nav_order') }}" id="navOrderForm">
|
|
||||||
<ul class="nav-order-list" id="navOrderList">
|
|
||||||
{% for item in nav_items_ordered %}
|
|
||||||
<li data-key="{{ item.key }}">
|
|
||||||
<span>{{ item.label }}</span>
|
|
||||||
<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>
|
|
||||||
<input type="hidden" name="nav_order" value="{{ item.key }}">
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<div class="card card-pad" style="max-width:420px; margin-top:24px;">
|
|
||||||
<div class="section-head" style="margin-bottom:16px;">
|
<div class="section-head" style="margin-bottom:16px;">
|
||||||
<div>
|
<div>
|
||||||
<h2 style="font-size:16px;">Prüfintervall</h2>
|
<h2 style="font-size:16px;">Prüfintervall</h2>
|
||||||
@@ -54,60 +23,4 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card card-pad" style="max-width:420px; margin-top:24px;">
|
|
||||||
<div class="section-head" style="margin-bottom:16px;">
|
|
||||||
<div>
|
|
||||||
<h2 style="font-size:16px;">Export</h2>
|
|
||||||
<div class="hint">Geräte, Switche und Zugangsdaten als verschlüsseltes Bundle sichern — z.B. für einen Umzug auf eine neue Umgebung.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form method="post" action="{{ url_for('export_data') }}">
|
|
||||||
<div class="field">
|
|
||||||
<label for="export_passphrase">Passphrase</label>
|
|
||||||
<input type="password" name="export_passphrase" id="export_passphrase" required>
|
|
||||||
<div class="field-hint">Wird zum Verschlüsseln der Export-Datei benötigt — für den späteren Import dieselbe Passphrase erneut eingeben.</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-secondary btn-block">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
|
|
||||||
Export herunterladen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card card-pad" style="max-width:420px; margin-top:24px;">
|
|
||||||
<div class="section-head" style="margin-bottom:16px;">
|
|
||||||
<div>
|
|
||||||
<h2 style="font-size:16px;">Import</h2>
|
|
||||||
<div class="hint">Ein zuvor exportiertes Bundle einlesen. Bestehende Einträge mit gleichem Namen/Hostname/MAC werden aktualisiert, neue werden angelegt.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form method="post" action="{{ url_for('import_data') }}" enctype="multipart/form-data"
|
|
||||||
data-confirm="Import wirklich starten? Bestehende Einträge mit gleichem Namen/Hostname/MAC werden überschrieben.">
|
|
||||||
<div class="field">
|
|
||||||
<label for="import_file">Export-Datei</label>
|
|
||||||
<input type="file" name="import_file" id="import_file" accept=".json" required>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="import_passphrase">Passphrase</label>
|
|
||||||
<input type="password" name="import_passphrase" id="import_passphrase" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-secondary btn-block">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 9V5a2 2 0 00-2-2H5a2 2 0 00-2 2v4"/><path d="M7 14l5-5 5 5"/><path d="M12 9v12"/></svg>
|
|
||||||
Import starten
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% 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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% set active_page = "settings_importexport" %}
|
||||||
|
{% block page_title %}Im-/Export{% endblock %}
|
||||||
|
{% block page_sub %}<div class="topbar-sub">Umzug auf eine neue Umgebung</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;">Export</h2>
|
||||||
|
<div class="hint">Geräte, Switche und Zugangsdaten als verschlüsseltes Bundle sichern — z.B. für einen Umzug auf eine neue Umgebung.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url_for('export_data') }}">
|
||||||
|
<div class="field">
|
||||||
|
<label for="export_passphrase">Passphrase</label>
|
||||||
|
<input type="password" name="export_passphrase" id="export_passphrase" required>
|
||||||
|
<div class="field-hint">Wird zum Verschlüsseln der Export-Datei benötigt — für den späteren Import dieselbe Passphrase erneut eingeben.</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-secondary btn-block">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
|
||||||
|
Export herunterladen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-pad">
|
||||||
|
<div class="section-head" style="margin-bottom:16px;">
|
||||||
|
<div>
|
||||||
|
<h2 style="font-size:16px;">Import</h2>
|
||||||
|
<div class="hint">Ein zuvor exportiertes Bundle einlesen. Bestehende Einträge mit gleichem Namen/Hostname/MAC werden aktualisiert, neue werden angelegt.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url_for('import_data') }}" enctype="multipart/form-data"
|
||||||
|
data-confirm="Import wirklich starten? Bestehende Einträge mit gleichem Namen/Hostname/MAC werden überschrieben.">
|
||||||
|
<div class="field">
|
||||||
|
<label for="import_file">Export-Datei</label>
|
||||||
|
<input type="file" name="import_file" id="import_file" accept=".json" required>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="import_passphrase">Passphrase</label>
|
||||||
|
<input type="password" name="import_passphrase" id="import_passphrase" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-secondary btn-block">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 9V5a2 2 0 00-2-2H5a2 2 0 00-2 2v4"/><path d="M7 14l5-5 5 5"/><path d="M12 9v12"/></svg>
|
||||||
|
Import starten
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user