Zugangsdaten-Entitaet, Gruppen-Tabelle mit Aufklappansicht, Vor-/Nachname, Dashboard-Sektionen

- Zugangsdaten (SSH-Logins) als eigenstaendige, wiederverwendbare Entitaet
  statt direkt am Switch; inline Neuanlage beim Switch-Erstellen moeglich;
  automatische Migration bestehender Switch-Logins
- Gruppen-Seite als Tabelle mit Aufklapp-Zeile fuer Rechte (Akkordeon),
  Admin als feste Systemzeile (Mitgliederverwaltung ueber is_admin),
  Standardgruppe 'Benutzer' mit allen Ansichtsrechten (devices.view,
  switches.view), automatische Zuordnung neuer/verwaister Benutzer
- Users-Seite: Icon-Buttons statt Text, Bearbeiten+Passwortaenderung in
  einem Modal zusammengefuehrt, Gruppe/Admin-Zuweisung ueber eigenen
  Zuweisen-Button (wie Switch-Zuordnung bei Devices), Vor-/Nachname
- Dashboard: einheitliche Kachelansicht mit/ohne Login, drei sortierte
  Abschnitte (Offline/Online/Deaktiviert), Kachel-Suchfilter, Bootstrap-
  artiges Grid (max. 6 Spalten), Aktivieren-Option im Popup fuer
  deaktivierte Geraete, Countdown serverseitig korrekt geseedet
- Sidebar dauerhaft einklappbar (Desktop, persistent via localStorage)
- Devices-Tabelle: Aktions-Buttons nebeneinander statt untereinander

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 13:30:47 +02:00
co-authored by Claude Sonnet 5
parent 08d8d1f738
commit fa6bb2935f
12 changed files with 1108 additions and 345 deletions
+5
View File
@@ -25,6 +25,7 @@
"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"/>',
"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"/>',
@@ -59,6 +60,10 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ icons['share']|safe }}</svg>
Switches
</a>
<a href="{{ url_for('credentials') }}" class="nav-item {% if active_page == 'credentials' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ icons['key']|safe }}</svg>
Zugangsdaten
</a>
{% endif %}
{% if current_user.is_admin %}
<a href="{{ url_for('users') }}" class="nav-item {% if active_page == 'users' %}active{% endif %}">
+157
View File
@@ -0,0 +1,157 @@
{% extends "base.html" %}
{% set active_page = "credentials" %}
{% set can_create = current_user.has_permission('switches.create') %}
{% set can_edit = current_user.has_permission('switches.edit') %}
{% set can_delete = current_user.has_permission('switches.delete') %}
{% block page_title %}Zugangsdaten{% endblock %}
{% block page_sub %}<div class="topbar-sub">{{ credentials|length }} Zugangsdaten</div>{% endblock %}
{% block topbar_right %}
{% if can_create %}
<button type="button" class="btn btn-primary" data-open-modal="addCredentialModal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
Neue Zugangsdaten
</button>
{% endif %}
{% endblock %}
{% block content %}
<p class="text-faint" style="font-size:12.5px; margin-bottom:18px; max-width:720px;">
Zugangsdaten können mehreren Switchen gleichzeitig zugeordnet werden. Beim Anlegen eines
Switches lassen sich bestehende Zugangsdaten auswählen oder direkt neue hinterlegen.
</p>
<div class="table-wrap">
<div style="overflow-x:auto;">
<table class="data-table">
<thead><tr><th>Name</th><th>Username</th><th>Verwendet von</th><th style="width:1%;">Aktionen</th></tr></thead>
<tbody>
{% for c in credentials %}
<tr>
<td class="cell-name">{{ c['name'] }}</td>
<td class="mono">{{ c['username'] }}</td>
<td class="text-dim">{{ c['switch_count'] }} Switch{{ 'e' if c['switch_count'] != 1 else '' }}</td>
<td>
<div class="row-actions">
{% if can_edit %}
<button class="icon-btn" title="Bearbeiten"
onclick="openEditCredentialModal({{ c['id'] }}, '{{ c['name'] }}', '{{ c['username'] }}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg>
</button>
{% endif %}
{% if can_delete %}
<form method="post" data-confirm="Zugangsdaten „{{ c['name'] }}“ wirklich löschen?">
<input type="hidden" name="delete_credential" value="{{ c['id'] }}">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr class="empty-row"><td colspan="4">Noch keine Zugangsdaten vorhanden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if can_create %}
<!-- Modal: Neue Zugangsdaten -->
<div class="modal-overlay" id="addCredentialModal">
<div class="modal" style="max-width:380px;">
<form method="post" onsubmit="return validateCredentialForm(this, 'add');">
<div class="modal-header">
<h3>Neue Zugangsdaten</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<input type="hidden" name="add_credential" value="1">
<div class="field"><label>Name</label>
<input type="text" name="name" required placeholder="z.B. Standard-Switch-Login">
</div>
<div class="field"><label>Username</label>
<input type="text" name="username" required placeholder="z.B. admin">
</div>
<div class="field"><label>Passwort</label>
<input type="password" id="password_add" name="password" required>
</div>
<div class="field"><label>Passwort bestätigen</label>
<input type="password" id="password_confirm_add" name="password_confirm" required>
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Anlegen</button>
</div>
</form>
</div>
</div>
{% endif %}
{% if can_edit %}
<!-- Modal: Zugangsdaten bearbeiten -->
<div class="modal-overlay" id="editCredentialModal">
<div class="modal" style="max-width:380px;">
<form method="post" onsubmit="return validateCredentialForm(this, 'edit');">
<input type="hidden" name="edit_credential" value="1">
<input type="hidden" name="credential_id" id="edit_cred_id">
<div class="modal-header">
<h3>Zugangsdaten bearbeiten</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="field"><label>Name</label>
<input type="text" name="name" id="edit_cred_name" required>
</div>
<div class="field"><label>Username</label>
<input type="text" name="username" id="edit_cred_username" required>
</div>
<div class="field"><label>Neues Passwort</label>
<input type="password" id="password_edit" name="password" placeholder="Nur bei Änderung ausfüllen">
</div>
<div class="field"><label>Passwort bestätigen</label>
<input type="password" id="password_confirm_edit" name="password_confirm" placeholder="Bestätigen">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
function openEditCredentialModal(id, name, username) {
document.getElementById("edit_cred_id").value = id;
document.getElementById("edit_cred_name").value = name;
document.getElementById("edit_cred_username").value = username;
document.getElementById("password_edit").value = "";
document.getElementById("password_confirm_edit").value = "";
PoeUI.openModal("editCredentialModal");
}
function validateCredentialForm(form, id) {
const pass = document.getElementById("password_" + id);
const confirm = document.getElementById("password_confirm_" + id);
if (!pass || !confirm) return true;
if (pass.value || confirm.value || id === "add") {
if (pass.value !== confirm.value) {
confirm.classList.add("is-invalid");
return false;
}
}
confirm.classList.remove("is-invalid");
return true;
}
</script>
{% endblock %}
+173 -105
View File
@@ -12,123 +12,179 @@
{% block content %}
<p class="text-faint" style="font-size:12.5px; margin-bottom:18px; max-width:720px;">
Admins dürfen immer alles. Über Gruppen lassen sich einzelne Verwaltungsrechte für
Devices und Switches gezielt an normale Benutzer vergeben, ohne sie zu Admins zu machen.
Ein Benutzer kann mehreren Gruppen angehören — die Rechte addieren sich.
Über Gruppen lassen sich einzelne Verwaltungsrechte für Devices und Switches gezielt vergeben.
Ein Benutzer kann mehreren Gruppen angehören — die Rechte addieren sich. Auf „Rechte“ klicken,
um eine Gruppe aufzuklappen und die Berechtigungen im Detail zu sehen bzw. zu ändern.
</p>
{# Virtuelle Admin-"Gruppe": rein informativ, kein Formular — Admin-Status
wird ausschließlich über den is_admin-Schalter auf der Users-Seite gesetzt. #}
<div class="card card-pad" style="margin-bottom:16px; opacity:0.85;">
<div class="section-head">
<div class="flex gap-2" style="align-items:center;">
<h3 style="font-size:16px;">Admin</h3>
<span class="pill admin">Systemrolle</span>
</div>
</div>
<p class="text-faint" style="font-size:12px; margin:-8px 0 14px;">
Admins dürfen immer alles — Rolle wird über <a href="{{ url_for('users') }}" style="color:var(--accent-strong); font-weight:600;">Users</a> vergeben, nicht hier.
</p>
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:24px;">
{% for cat_key, cat in permission_catalog.items() %}
<div>
<div class="permission-group-title">{{ cat['label'] }}</div>
<div class="check-list">
{% for key, label in cat['items'].items() %}
<label class="check-row" style="cursor:default; color:var(--text-faint);">
<input type="checkbox" checked disabled>
{{ label }}
</label>
{% endfor %}
</div>
</div>
{% endfor %}
<div>
<div class="permission-group-title">Mitglieder</div>
<p class="text-dim" style="font-size:13px;">
{% if admin_virtual_group.member_names %}{{ admin_virtual_group.member_names|join(', ') }}{% else %}Keine Admins vorhanden.{% endif %}
</p>
</div>
<div class="table-wrap">
<div style="overflow-x:auto;">
<table class="data-table">
<thead><tr><th>Gruppe</th><th>Mitglieder</th><th style="width:1%;">Aktionen</th></tr></thead>
<tbody>
<!-- Virtuelle "Admin"-Gruppe: Rechte sind fix (alles), Mitgliedschaft
wird direkt über is_admin gesteuert. -->
<tr>
<td class="cell-name">Admin <span class="pill admin">Systemrolle</span></td>
<td class="text-dim">{{ admin_virtual_group.member_names|length }}</td>
<td>
<div class="row-actions">
<button class="icon-btn" title="Rechte anzeigen" onclick="toggleDetail('detail-admin')">
<svg id="chev-admin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
</button>
<button class="icon-btn" title="Mitglieder verwalten" data-open-modal="adminMembersModal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>
</button>
</div>
</td>
</tr>
<tr class="group-detail-row hidden" id="detail-admin">
<td colspan="3">
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:24px;">
{% for cat_key, cat in permission_catalog.items() %}
<div>
<div class="permission-group-title">{{ cat['label'] }}</div>
<div class="check-list">
{% for key, label in cat['items'].items() %}
<label class="check-row" style="cursor:default; color:var(--text-faint);">
<input type="checkbox" checked disabled>
{{ label }}
</label>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<p class="text-faint" style="font-size:11.5px; margin:12px 0 0;">Admins dürfen immer alles — diese Rechte sind fest und nicht änderbar.</p>
</td>
</tr>
{% for g in groups %}
<tr>
<td class="cell-name">
{{ g.name }}
{% if g.is_default %}<span class="pill user" style="white-space:nowrap;">Standard</span>{% endif %}
</td>
<td class="text-dim">{{ g.member_names|length }}</td>
<td>
<div class="row-actions">
<button class="icon-btn" title="Rechte anzeigen/bearbeiten" onclick="toggleDetail('detail-{{ g.id }}')">
<svg id="chev-{{ g.id }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
</button>
<button class="icon-btn" title="Mitglieder verwalten" data-open-modal="membersModal{{ loop.index }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>
</button>
{% if not g.is_default %}
<form method="post" data-confirm="Gruppe „{{ g.name }}“ wirklich löschen? Mitglieder verlieren die zugehörigen Rechte.">
<input type="hidden" name="delete_group" value="{{ g.id }}">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
{% endif %}
</div>
</td>
</tr>
<tr class="group-detail-row hidden" id="detail-{{ g.id }}">
<td colspan="3">
<form method="post">
<input type="hidden" name="save_group" value="1">
<input type="hidden" name="permissions_submitted" value="1">
<input type="hidden" name="group_id" value="{{ g.id }}">
<input type="hidden" name="name" value="{{ g.name }}">
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:24px;">
{% for cat_key, cat in permission_catalog.items() %}
<div>
<div class="permission-group-title">{{ cat['label'] }}</div>
<div class="check-list">
{% for key, label in cat['items'].items() %}
<label class="check-row">
<input type="checkbox" name="permissions" value="{{ key }}" {% if key in g.permissions %}checked{% endif %}>
{{ label }}
</label>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="flex" style="justify-content:flex-end; margin-top:16px;">
<button type="submit" class="btn btn-primary btn-sm">
<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>
Rechte speichern
</button>
</div>
</form>
</td>
</tr>
{% else %}
<tr class="empty-row"><td colspan="3">Noch keine weiteren Gruppen angelegt.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if groups %}
<div style="display:flex; flex-direction:column; gap:16px;">
{% for g in groups %}
<div class="card card-pad">
<!-- Modal: Admin-Mitglieder verwalten -->
<div class="modal-overlay" id="adminMembersModal">
<div class="modal" style="max-width:380px;">
<form method="post">
<input type="hidden" name="save_group" value="1">
<input type="hidden" name="group_id" value="{{ g.id }}">
<div class="section-head">
<div class="field" style="margin-bottom:0; max-width:280px; flex:1;">
<label>Gruppenname</label>
<div class="flex gap-2" style="align-items:center;">
<input type="text" name="name" value="{{ g.name }}" required>
{% if g.is_default %}<span class="pill user" style="white-space:nowrap;">Standard</span>{% endif %}
</div>
</div>
<button type="submit" 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="M20 6L9 17l-5-5"/></svg>
Speichern
</button>
<input type="hidden" name="assign_admins" value="1">
<div class="modal-header">
<h3>Admin-Mitglieder</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
{% if g.is_default %}
<p class="text-faint" style="font-size:11.5px; margin:-8px 0 8px;">
Standardgruppe — jeder neu angelegte Benutzer wird ihr automatisch zugeordnet. Kann nicht gelöscht werden.
</p>
{% endif %}
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:24px; margin-top:8px;">
{% for cat_key, cat in permission_catalog.items() %}
<div>
<div class="permission-group-title">{{ cat['label'] }}</div>
<div class="check-list">
{% for key, label in cat['items'].items() %}
<label class="check-row">
<input type="checkbox" name="permissions" value="{{ key }}" {% if key in g.permissions %}checked{% endif %}>
{{ label }}
</label>
{% endfor %}
</div>
</div>
{% endfor %}
<div>
<div class="permission-group-title">Mitglieder</div>
<div class="check-list" style="max-height:180px; overflow-y:auto; padding-right:4px;">
{% for u in all_users %}
<label class="check-row">
<input type="checkbox" name="members" value="{{ u['id'] }}" {% if u['id'] in g.members %}checked{% endif %}>
{{ u['username'] }}
</label>
{% else %}
<p class="text-faint" style="font-size:12px;">Keine Nicht-Admin-Benutzer vorhanden.</p>
{% endfor %}
</div>
<div class="modal-body">
<p class="text-faint" style="font-size:11.5px; margin:0 0 12px;">Mindestens ein Admin muss bestehen bleiben.</p>
<div class="check-list" style="max-height:320px; overflow-y:auto; padding-right:4px;">
{% for u in all_users_all %}
<label class="check-row">
<input type="checkbox" name="members" value="{{ u['id'] }}" {% if u['is_admin'] %}checked{% endif %}>
{{ u['username'] }}
</label>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
<div class="flex" style="justify-content:space-between; align-items:center; margin-top:16px; padding-top:14px; border-top:1px solid var(--border-soft);">
<span class="text-faint" style="font-size:12px;">
{% if g.member_names %}Mitglieder: {{ g.member_names|join(', ') }}{% else %}Keine Mitglieder{% endif %}
</span>
{% if not g.is_default %}
<form method="post" data-confirm="Gruppe „{{ g.name }}“ wirklich löschen? Mitglieder verlieren die zugehörigen Rechte.">
<input type="hidden" name="delete_group" value="{{ g.id }}">
<button type="submit" class="btn btn-sm btn-danger">Gruppe löschen</button>
</form>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card card-pad" style="text-align:center; color:var(--text-faint);">
Noch keine Gruppen angelegt.
<!-- Modals: Mitglieder pro Gruppe verwalten -->
{% for g in groups %}
<div class="modal-overlay" id="membersModal{{ loop.index }}">
<div class="modal" style="max-width:380px;">
<form method="post">
<input type="hidden" name="save_group" value="1">
<input type="hidden" name="members_submitted" value="1">
<input type="hidden" name="group_id" value="{{ g.id }}">
<input type="hidden" name="name" value="{{ g.name }}">
<div class="modal-header">
<h3>Mitglieder — {{ g.name }}</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="check-list" style="max-height:320px; overflow-y:auto; padding-right:4px;">
{% for u in all_users %}
<label class="check-row">
<input type="checkbox" name="members" value="{{ u['id'] }}" {% if u['id'] in g.members %}checked{% endif %}>
{{ u['username'] }}
</label>
{% else %}
<p class="text-faint" style="font-size:12px;">Keine Nicht-Admin-Benutzer vorhanden.</p>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
{% endif %}
{% endfor %}
<!-- Modal: Neue Gruppe -->
<div class="modal-overlay" id="addGroupModal">
@@ -143,7 +199,7 @@
<div class="field">
<label>Name</label>
<input type="text" name="name" required placeholder="z.B. Facility-Team">
<div class="field-hint">Rechte und Mitglieder werden danach auf der Gruppenkarte eingestellt.</div>
<div class="field-hint">Rechte und Mitglieder werden danach über die Gruppentabelle eingestellt.</div>
</div>
</div>
<div class="modal-footer">
@@ -155,3 +211,15 @@
</div>
{% endblock %}
{% block scripts %}
<script>
function toggleDetail(id) {
const row = document.getElementById(id);
if (!row) return;
row.classList.toggle("hidden");
const chev = document.getElementById(id.replace("detail-", "chev-"));
if (chev) chev.style.transform = row.classList.contains("hidden") ? "" : "rotate(180deg)";
}
</script>
{% endblock %}
+114 -30
View File
@@ -1,8 +1,12 @@
{% extends "base.html" %}
{% set active_page = "index" %}
{% block page_title %}Dashboard{% endblock %}
{% block page_sub %}<div class="topbar-sub">{{ devices|length }} Geräte{{ " im Bestand" if current_user.is_authenticated else "" }}</div>{% endblock %}
{% block page_sub %}<div class="topbar-sub">{{ device_count }} Geräte{{ " im Bestand" if current_user.is_authenticated else "" }}</div>{% endblock %}
{% block topbar_right %}
<div class="search-input" style="min-width:180px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="text" id="tileSearch" placeholder="Gerät suchen…">
</div>
<span class="timer-pill"><span class="dot"></span><span id="dashboard-timer">Nächste Prüfung in --s</span></span>
{% endblock %}
@@ -29,35 +33,64 @@
</div>
</div>
{% if devices %}
<div class="device-grid">
{% for d in devices %}
{% set st = status.get(d['mac'], 'unbekannt') %}
<div class="device-card {% if d['is_active'] == 0 %}is-disabled{% endif %} {% if not current_user.is_authenticated %}is-readonly{% endif %}"
data-status="{{ 'offline' if d['is_active'] == 0 else st }}"
data-mac="{{ d['mac'] }}"
data-name="{{ d['name'] }}"
data-ip="{{ d['rpi_ip'] }}"
data-switch="{{ d['switch_hostname'] or '-' }}"
data-port="{{ d['port'] or '-' }}"
data-active="{{ d['is_active'] }}"
data-checked="{{ last_checked.get(d['mac']) or '-' }}"
{% if last_seen.get(d['mac']) %}title="{{ last_seen[d['mac']] }}"{% endif %}>
<div class="dc-top">
<div class="dc-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2.5"/><path d="M8 2v3M16 2v3M8 19v3M16 19v3M2 8h3M2 16h3M19 8h3M19 16h3"/></svg>
</div>
{% if d['is_active'] == 0 %}
<span class="pill disabled">Aus</span>
{% else %}
<span class="pill {{ st }}">{{ {'online':'Online','offline':'Offline','unbekannt':'Unbekannt'}[st] }}</span>
{% endif %}
{% macro device_tile(d) %}
{% set st = status.get(d['mac'], 'unbekannt') %}
<div class="device-card {% if d['is_active'] == 0 %}is-disabled{% endif %} {% if not current_user.is_authenticated %}is-readonly{% endif %}"
data-status="{{ 'offline' if d['is_active'] == 0 else st }}"
data-mac="{{ d['mac'] }}"
data-name="{{ d['name'] }}"
data-ip="{{ d['rpi_ip'] }}"
data-switch="{{ d['switch_hostname'] or '-' }}"
data-port="{{ d['port'] or '-' }}"
data-active="{{ d['is_active'] }}"
data-checked="{{ last_checked.get(d['mac']) or '-' }}"
{% if last_seen.get(d['mac']) %}title="{{ last_seen[d['mac']] }}"{% endif %}>
<div class="dc-top">
<div class="dc-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2.5"/><path d="M8 2v3M16 2v3M8 19v3M16 19v3M2 8h3M2 16h3M19 8h3M19 16h3"/></svg>
</div>
<div class="dc-name">{{ d['name'] }}</div>
<div class="dc-ip">{{ d['rpi_ip'] }}</div>
{% if d['is_active'] == 0 %}
<span class="pill disabled">Aus</span>
{% else %}
<span class="pill {{ st }}">{{ {'online':'Online','offline':'Offline','unbekannt':'Unbekannt'}[st] }}</span>
{% endif %}
</div>
{% endfor %}
<div class="dc-name">{{ d['name'] }}</div>
<div class="dc-ip">{{ d['rpi_ip'] }}</div>
</div>
{% endmacro %}
{% if device_count %}
{% if offline_devices %}
<div class="dash-section">
<div class="dash-section-title">Offline <span class="pill offline">{{ offline_devices|length }}</span></div>
<div class="device-grid">
{% for d in offline_devices %}{{ device_tile(d) }}{% endfor %}
</div>
</div>
{% endif %}
{% if online_devices %}
<div class="dash-section">
<div class="dash-section-title">Online <span class="pill online">{{ online_devices|length }}</span></div>
<div class="device-grid">
{% for d in online_devices %}{{ device_tile(d) }}{% endfor %}
</div>
</div>
{% endif %}
{% if disabled_devices %}
<div class="dash-section">
<div class="dash-section-title">Deaktiviert <span class="pill disabled">{{ disabled_devices|length }}</span></div>
<div class="device-grid">
{% for d in disabled_devices %}{{ device_tile(d) }}{% endfor %}
</div>
</div>
{% endif %}
<p id="noSearchResults" class="text-faint hidden" style="text-align:center; padding:30px 0;">Kein Gerät gefunden.</p>
{% else %}
<div class="card card-pad" style="text-align:center; color:var(--text-faint);">
{% if current_user.is_authenticated %}
@@ -95,8 +128,14 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Schließen</button>
{% if current_user.has_permission('devices.toggle') %}
<button type="button" class="btn btn-success" id="activateButton" style="display:none;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5L20 7"/></svg>
Aktivieren
</button>
{% endif %}
{% if current_user.has_permission('devices.restart') %}
<button type="button" class="btn btn-primary" id="restartButton">
<button type="button" class="btn btn-primary" id="restartButton" style="display:none;">
<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>
Neustarten
</button>
@@ -153,11 +192,35 @@ document.addEventListener("DOMContentLoaded", () => {
setInterval(updateTimer, 1000);
updateTimer();
// Suchfilter über alle Abschnitte hinweg; Abschnitt wird komplett
// ausgeblendet, wenn keine seiner Kacheln mehr passt.
const searchInput = document.getElementById("tileSearch");
if (searchInput) {
searchInput.addEventListener("input", () => {
const q = searchInput.value.trim().toLowerCase();
let anyVisible = false;
document.querySelectorAll(".dash-section").forEach(section => {
let sectionHasMatch = false;
section.querySelectorAll(".device-card").forEach(card => {
const match = card.dataset.name.toLowerCase().includes(q);
card.classList.toggle("hidden", !match);
if (match) sectionHasMatch = true;
});
section.classList.toggle("hidden", !sectionHasMatch);
if (sectionHasMatch) anyVisible = true;
});
const noResults = document.getElementById("noSearchResults");
if (noResults) noResults.classList.toggle("hidden", anyVisible || q === "");
});
}
if (!isAuthenticated) {
return;
}
let selectedCard = null, selectedMac = null, selectedName = null;
const restartButton = document.getElementById("restartButton");
const activateButton = document.getElementById("activateButton");
document.querySelectorAll(".device-card").forEach(card => {
const mac = card.dataset.mac;
@@ -171,11 +234,11 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
card.addEventListener("click", function () {
if (this.dataset.active == "0") return;
if (sessionStorage.getItem("restart_" + this.dataset.mac)) return;
selectedCard = this;
selectedMac = this.dataset.mac;
selectedName = this.dataset.name;
const isActive = this.dataset.active != "0";
document.getElementById("deviceModalTitle").innerText = this.dataset.name;
document.getElementById("deviceIp").innerText = this.dataset.ip || "-";
document.getElementById("deviceSwitch").innerText = this.dataset.switch || "-";
@@ -183,11 +246,12 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("deviceChecked").innerText = this.dataset.checked || "-";
const pill = this.querySelector(".pill");
document.getElementById("deviceStatus").innerText = pill ? pill.innerText.trim() : "-";
if (restartButton) restartButton.style.display = isActive ? "" : "none";
if (activateButton) activateButton.style.display = isActive ? "none" : "";
PoeUI.openModal("deviceModal");
});
});
const restartButton = document.getElementById("restartButton");
if (restartButton) {
restartButton.addEventListener("click", function () {
PoeUI.closeModal(document.getElementById("deviceModal"));
@@ -219,6 +283,26 @@ document.addEventListener("DOMContentLoaded", () => {
.catch(err => { button.disabled = false; showToast("Fehler beim Starten des Neustarts.", "danger"); });
});
}
if (activateButton) {
activateButton.addEventListener("click", function () {
const button = this;
button.disabled = true;
fetch("/devices/toggle/" + encodeURIComponent(selectedMac), { method: "POST" })
.then(r => r.json())
.then(data => {
button.disabled = false;
if (!data.success) {
showToast(data.msg || "Aktivieren fehlgeschlagen.", "danger");
return;
}
PoeUI.closeModal(document.getElementById("deviceModal"));
showToast(data.msg, "success");
setTimeout(() => window.location.reload(), 500);
})
.catch(() => { button.disabled = false; showToast("Fehler beim Aktivieren.", "danger"); });
});
}
});
</script>
{% endblock %}
+101 -32
View File
@@ -10,7 +10,7 @@
{% block page_sub %}<div class="topbar-sub">{{ switches|length }} Switche</div>{% endblock %}
{% block topbar_right %}
{% if can_create %}
<button type="button" class="btn btn-primary" data-open-modal="addSwitchModal">
<button type="button" class="btn btn-primary" data-open-modal="addSwitchModal" onclick="resetCredentialChoice('add')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
Neuer Switch
</button>
@@ -29,18 +29,24 @@
<div style="overflow-x:auto;">
<table class="data-table" id="switchesTable">
<thead>
<tr><th>Hostname</th><th>IP-Adresse</th><th>Username</th><th style="width:1%;">Aktionen</th></tr>
<tr><th>Hostname</th><th>IP-Adresse</th><th>Zugangsdaten</th><th style="width:1%;">Aktionen</th></tr>
</thead>
<tbody>
{% for s in switches %}
<tr>
<td class="cell-name">{{ s['hostname'] }}</td>
<td class="mono">{{ s['ip'] }}</td>
<td>{{ s['username'] }}</td>
<td>
{% if s['credential_name'] %}
{{ s['credential_name'] }} <span class="text-faint mono" style="font-size:11.5px;">({{ s['credential_username'] }})</span>
{% else %}
<span class="text-faint">— keine —</span>
{% endif %}
</td>
<td>
<div class="row-actions">
{% if can_edit %}
<button class="icon-btn" title="Bearbeiten" data-open-modal="editSwitchModal{{ loop.index }}">
<button class="icon-btn" title="Bearbeiten" data-open-modal="editSwitchModal{{ loop.index }}" onclick="resetCredentialChoice('edit{{ loop.index }}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg>
</button>
{% endif %}
@@ -58,7 +64,7 @@
{% if can_edit %}
<div class="modal-overlay" id="editSwitchModal{{ loop.index }}">
<div class="modal">
<form method="post" onsubmit="return validateSwitchForm(this, '{{ loop.index }}');">
<form method="post" onsubmit="return validateSwitchForm(this, 'edit{{ loop.index }}');">
<input type="hidden" name="edit_switch" value="1">
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
<div class="modal-header">
@@ -73,15 +79,29 @@
<input type="text" name="ip" value="{{ s['ip'] }}" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div>
</div>
<div class="field"><label>Username</label>
<input type="text" name="username" value="{{ s['username'] }}" required>
<div class="field">
<label>Zugangsdaten</label>
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
{% for c in all_credentials %}
<option value="{{ c['id'] }}" data-username="{{ c['username'] }}" {% if c['id'] == s['credential_id'] %}selected{% endif %}>{{ c['name'] }}</option>
{% endfor %}
<option value="new">+ Neue Zugangsdaten anlegen</option>
</select>
</div>
<div class="field"><label>Neues Passwort</label>
<input type="password" id="password_edit_{{ loop.index }}" name="password" placeholder="Nur bei Änderung ausfüllen">
</div>
<div class="field"><label>Passwort bestätigen</label>
<input type="password" id="password_confirm_edit_{{ loop.index }}" name="password_confirm" placeholder="Bestätigen">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
<div class="new-credential-fields hidden">
<div class="field"><label>Name der Zugangsdaten</label>
<input type="text" name="new_credential_name" placeholder="z.B. Lager-Switche">
</div>
<div class="field"><label>Username</label>
<input type="text" name="new_credential_username" placeholder="z.B. admin">
</div>
<div class="field"><label>Passwort</label>
<input type="password" id="password_edit{{ loop.index }}" name="new_credential_password">
</div>
<div class="field"><label>Passwort bestätigen</label>
<input type="password" id="password_confirm_edit{{ loop.index }}" name="new_credential_password_confirm">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
</div>
<div class="modal-footer">
@@ -122,15 +142,32 @@
<input type="text" name="ip" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div>
</div>
<div class="field"><label>Username</label>
<input type="text" name="username" required placeholder="z.B. admin">
<div class="field">
<label>Zugangsdaten</label>
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
{% if not all_credentials %}<option value="new" selected>+ Neue Zugangsdaten anlegen</option>
{% else %}
{% for c in all_credentials %}
<option value="{{ c['id'] }}" data-username="{{ c['username'] }}">{{ c['name'] }}</option>
{% endfor %}
<option value="new">+ Neue Zugangsdaten anlegen</option>
{% endif %}
</select>
</div>
<div class="field"><label>Passwort</label>
<input type="password" id="password_add" name="password" required>
</div>
<div class="field"><label>Passwort bestätigen</label>
<input type="password" id="password_confirm_add" name="password_confirm" required>
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
<div class="new-credential-fields {% if all_credentials %}hidden{% endif %}">
<div class="field"><label>Name der Zugangsdaten</label>
<input type="text" name="new_credential_name" placeholder="z.B. Lager-Switche">
</div>
<div class="field"><label>Username</label>
<input type="text" name="new_credential_username" placeholder="z.B. admin">
</div>
<div class="field"><label>Passwort</label>
<input type="password" id="password_add" name="new_credential_password">
</div>
<div class="field"><label>Passwort bestätigen</label>
<input type="password" id="password_confirm_add" name="new_credential_password_confirm">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
</div>
<div class="modal-footer">
@@ -171,6 +208,7 @@
<div class="modal-footer">
<p class="text-faint" style="font-size:11.5px; margin-right:auto;">
Erste Verbindung? Bestätige den Host-Key mit „yes“, gib danach das Passwort ein — direkt hier im Terminal.
Bei bestehenden Zugangsdaten ist das Passwort hier nicht bekannt (verschlüsselt gespeichert) — bitte manuell eingeben.
</p>
<button type="button" class="btn btn-secondary" data-close-modal onclick="closeTerminal()">Schließen</button>
</div>
@@ -184,6 +222,23 @@
<script src="{{ url_for('static', filename='js/vendor/xterm.js') }}"></script>
<script src="{{ url_for('static', filename='js/vendor/xterm-addon-fit.js') }}"></script>
<script>
// -------------------------------------------------------------------------
// Zugangsdaten-Auswahl: "+ Neue Zugangsdaten anlegen" blendet die Felder ein
// -------------------------------------------------------------------------
function toggleNewCredentialFields(select) {
const fields = select.closest(".modal-body").querySelector(".new-credential-fields");
if (fields) fields.classList.toggle("hidden", select.value !== "new");
}
function resetCredentialChoice(id) {
// Beim Öffnen sicherstellen, dass die "Neue Zugangsdaten"-Felder passend
// zur aktuellen Auswahl ein-/ausgeblendet sind (relevant v.a. nach
// vorherigem Umschalten auf "neu" ohne zu speichern).
setTimeout(() => {
const select = document.querySelector(`#${id === 'add' ? 'addSwitchModal' : 'editSwitchModal' + id.replace('edit','')} .credential-select`);
if (select) toggleNewCredentialFields(select);
}, 0);
}
// -------------------------------------------------------------------------
// SSH-Verbindungstest (Web-Terminal via /ws/ssh_terminal)
// -------------------------------------------------------------------------
@@ -221,13 +276,25 @@ function setTermStatus(text, cls) {
el.innerText = text;
}
function getCredentialInfo(form) {
const select = form.querySelector(".credential-select");
if (!select || select.value === "new") {
return {
username: (form.querySelector("input[name='new_credential_username']") || {}).value?.trim(),
passwordInput: form.querySelector("input[name='new_credential_password']"),
};
}
const option = select.selectedOptions[0];
return { username: option ? option.dataset.username : null, passwordInput: null };
}
function openTerminal(form) {
const host = (form.querySelector("input[name='ip']") || {}).value?.trim();
const username = (form.querySelector("input[name='username']") || {}).value?.trim();
activePasswordInput = form.querySelector("input[name='password']");
const { username, passwordInput } = getCredentialInfo(form);
activePasswordInput = passwordInput;
if (!host || !username) {
showToast("Bitte IP-Adresse und Username ausfüllen, bevor du die Verbindung testest.", "danger");
showToast("Bitte IP-Adresse ausfüllen und Zugangsdaten auswählen/anlegen, bevor du die Verbindung testest.", "danger");
return;
}
@@ -267,7 +334,7 @@ const termPasteBtn = document.getElementById("termPastePassword");
if (termPasteBtn) {
termPasteBtn.addEventListener("click", () => {
if (!activePasswordInput || !activePasswordInput.value) {
showToast("Kein Passwort im Formular eingetragen.", "danger");
showToast("Kein Passwort bekannt — bitte manuell im Terminal eingeben.", "danger");
return;
}
if (!termSocket || termSocket.readyState !== WebSocket.OPEN) {
@@ -297,9 +364,11 @@ function validateIP(input) {
return ok;
}
function validatePassword(id, isEdit) {
const pass = document.getElementById(isEdit ? `password_edit_${id}` : "password_add");
const confirm = document.getElementById(isEdit ? `password_confirm_edit_${id}` : "password_confirm_add");
function validateNewCredentialPassword(form) {
const select = form.querySelector(".credential-select");
if (!select || select.value !== "new") return true;
const pass = form.querySelector("input[name='new_credential_password']");
const confirm = form.querySelector("input[name='new_credential_password_confirm']");
if (!pass || !confirm) return true;
if (pass.value !== confirm.value) { confirm.classList.add("is-invalid"); return false; }
confirm.classList.remove("is-invalid");
@@ -310,15 +379,15 @@ function validateSwitchForm(form, id) {
const ipInput = form.querySelector("input[name='ip']");
let valid = true;
if (ipInput) valid = validateIP(ipInput) && valid;
valid = validatePassword(id, id !== "add") && valid;
valid = validateNewCredentialPassword(form) && valid;
return valid;
}
document.addEventListener("input", (e) => {
if (e.target.name === "ip") validateIP(e.target);
if (e.target.id.startsWith("password_confirm")) {
const id = e.target.id.replace("password_confirm_", "");
const pass = document.getElementById("password_" + id);
if (e.target.name === "new_credential_password_confirm") {
const form = e.target.closest("form");
const pass = form.querySelector("input[name='new_credential_password']");
if (pass) e.target.classList.toggle("is-invalid", pass.value !== e.target.value);
}
});
+66 -35
View File
@@ -14,17 +14,31 @@
<div class="table-wrap">
<div style="overflow-x:auto;">
<table class="data-table">
<thead><tr><th>Username</th><th>Rolle</th><th>Gruppen</th><th style="width:1%;">Aktionen</th></tr></thead>
<thead><tr><th>Username</th><th>Name</th><th>Gruppe</th><th style="width:1%;">Aktionen</th></tr></thead>
<tbody>
{% for u in users %}
<tr>
<td class="cell-name">{{ u['username'] }}</td>
<td><span class="pill {{ 'admin' if u['is_admin'] else 'user' }}">{{ "Admin" if u['is_admin'] else "User" }}</span></td>
<td class="text-dim">{{ u['group_names'] or '—' }}</td>
<td class="text-dim">
{% if u['first_name'] or u['last_name'] %}{{ [u['first_name'], u['last_name']]|select|join(' ') }}{% else %}—{% endif %}
</td>
<td>
{% if u['is_admin'] %}
<span class="pill admin">Admin</span>
{% else %}
<span class="text-dim">{{ u['group_names'] or '—' }}</span>
{% endif %}
</td>
<td>
<div class="row-actions">
<button class="btn btn-sm btn-secondary" onclick="openRoleModal({{ u['id'] }}, '{{ u['username'] }}', {{ u['is_admin'] }})">Rolle</button>
<button class="btn btn-sm btn-secondary" onclick="openPasswordModal({{ u['id'] }}, '{{ u['username'] }}')">Passwort</button>
<button class="icon-btn" title="Bearbeiten"
onclick="openEditModal({{ u['id'] }}, '{{ u['username'] }}', '{{ u['first_name'] or '' }}', '{{ u['last_name'] or '' }}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg>
</button>
<button class="icon-btn" title="Gruppe zuweisen"
onclick="openGroupModal({{ u['id'] }}, '{{ 'admin' if u['is_admin'] else (u['group_id'] or '') }}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>
</button>
<form method="post" data-confirm="Willst du „{{ u['username'] }}“ wirklich löschen?">
<button type="submit" name="delete_user" value="{{ u['id'] }}" class="icon-btn" style="color:var(--danger);" title="Löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
@@ -50,10 +64,16 @@
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="field"><label>Vorname</label><input type="text" name="first_name"></div>
<div class="field"><label>Name</label><input type="text" name="last_name"></div>
<div class="field"><label>Username</label><input type="text" name="username" required></div>
<div class="field"><label>Passwort</label><input type="password" name="password" required></div>
<div class="field"><label>Rolle</label>
<select name="is_admin"><option value="0">User</option><option value="1">Admin</option></select>
<div class="field">
<label>Gruppe</label>
<select name="group_id">
{% for g in all_groups %}<option value="{{ g['id'] }}" {% if g['is_default'] %}selected{% endif %}>{{ g['name'] }}</option>{% endfor %}
<option value="admin">Admin</option>
</select>
</div>
</div>
<div class="modal-footer">
@@ -64,45 +84,55 @@
</div>
</div>
<!-- Modal: Rolle ändern -->
<div class="modal-overlay" id="roleModal">
<!-- Modal: Benutzer bearbeiten (Stammdaten + optional neues Passwort;
Gruppe/Rolle wird ausschließlich über "Gruppe zuweisen" geändert) -->
<div class="modal-overlay" id="editModal">
<div class="modal">
<form method="post" id="roleForm">
<input type="hidden" name="user_id" id="role_user_id">
<form method="post" id="editForm">
<input type="hidden" name="user_id" id="edit_user_id">
<div class="modal-header">
<h3>Rolle ändern</h3>
<h3>Benutzer bearbeiten</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="field"><label>Username</label><input type="text" name="username" id="role_username" required></div>
<div class="field"><label>Rolle</label>
<select name="is_admin" id="role_is_admin"><option value="0">User</option><option value="1">Admin</option></select>
<div class="field"><label>Vorname</label><input type="text" name="first_name" id="edit_first_name"></div>
<div class="field"><label>Name</label><input type="text" name="last_name" id="edit_last_name"></div>
<div class="field"><label>Username</label><input type="text" name="username" id="edit_username" required></div>
<div class="field"><label>Neues Passwort</label>
<input type="password" name="new_password" placeholder="Nur bei Änderung ausfüllen">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" name="change_role" value="1" class="btn btn-primary">Speichern</button>
<button type="submit" name="edit_user" value="1" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
<!-- Modal: Passwort ändern -->
<div class="modal-overlay" id="passwordModal">
<div class="modal">
<form method="post" id="passwordForm">
<input type="hidden" name="user_id" id="password_user_id">
<!-- Modal: Gruppe zuweisen (inkl. Admin als Auswahl) -->
<div class="modal-overlay" id="groupModal">
<div class="modal" style="max-width:380px;">
<form method="post" id="groupForm">
<input type="hidden" name="user_id" id="group_user_id">
<div class="modal-header">
<h3>Passwort ändern</h3>
<h3>Gruppe zuweisen</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="field"><label>Username</label><input type="text" name="username" id="password_username" readonly></div>
<div class="field"><label>Neues Passwort</label><input type="password" name="new_password" required></div>
<div class="field">
<label>Gruppe</label>
<select name="group_id" id="group_select">
<option value="">Keine Gruppe</option>
{% for g in all_groups %}<option value="{{ g['id'] }}">{{ g['name'] }}</option>{% endfor %}
<option value="admin">Admin</option>
</select>
<div class="field-hint">Ersetzt die bisherige Gruppen-/Rollenzuordnung dieses Benutzers.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" name="change_password" value="1" class="btn btn-primary">Ändern</button>
<button type="submit" name="assign_group" value="1" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
@@ -112,17 +142,18 @@
{% block scripts %}
<script>
function openRoleModal(userId, username, isAdmin) {
document.getElementById("role_user_id").value = userId;
document.getElementById("role_username").value = username;
document.getElementById("role_is_admin").value = isAdmin;
PoeUI.openModal("roleModal");
function openEditModal(userId, username, firstName, lastName) {
document.getElementById("edit_user_id").value = userId;
document.getElementById("edit_username").value = username;
document.getElementById("edit_first_name").value = firstName;
document.getElementById("edit_last_name").value = lastName;
document.querySelector("#editForm input[name='new_password']").value = "";
PoeUI.openModal("editModal");
}
function openPasswordModal(userId, username) {
document.getElementById("password_user_id").value = userId;
document.getElementById("password_username").value = username;
document.querySelector("#passwordForm input[name='new_password']").value = "";
PoeUI.openModal("passwordModal");
function openGroupModal(userId, groupChoice) {
document.getElementById("group_user_id").value = userId;
document.getElementById("group_select").value = groupChoice || "";
PoeUI.openModal("groupModal");
}
</script>
{% endblock %}