Topbar auf globalen Prüf-Timer reduziert, Aktionen+Beschreibung über Tabellen, sortierbare Listen
- Topbar zeigt rechts jetzt ausschließlich den "Nächste Prüfung"-Countdown, konsistent auf jeder Seite (auch anonymes Dashboard) statt nur auf dem Dashboard. Neuer Context-Processor inject_check_timer()/get_last_run_at() liefert last_run/interval global, ohne dass jede Route das selbst berechnen muss. - "+ Neu ..."-Buttons (Devices, Switches, Zugangsdaten, Benutzer, Gruppen) aus der Topbar entfernt und stattdessen in einen .section-head direkt über der jeweiligen Tabelle verschoben, zusammen mit einer kurzen Beschreibung der Seite (bisher nur bei Zugangsdaten/Gruppen vorhanden, jetzt auch bei Geräte/Switche/Benutzer). - Live-Log: eigene lokale Timer-Pill entfernt (redundant zum globalen Timer), "Aktualisieren"-Button in denselben section-head verschoben. Dashboard: Suchfeld aus der Topbar in den Seiteninhalt verschoben, lokale Timer-Anzeige entfernt (übernimmt die globale Topbar-Pill), Reload-bei- Intervallende-Logik bleibt als separater, unsichtbarer Scheduler erhalten. - Neue generische Tabellen-Sortierung (app.js: initSortableTables): Klick auf eine Spaltenüberschrift mit data-sort-key sortiert die Zeilen anhand von data-sort-<key>-Attributen. Unterstützt auch Akkordeon-Tabellen mit mehreren <tbody> (Gruppen: Haupt- + Detail-Zeile bleiben als Einheit zusammen, die virtuelle "Admin"-Zeile bleibt über data-sort-pinned immer oben). Angewendet auf Geräte, Switche, Zugangsdaten, Benutzer, Gruppen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,7 +112,11 @@
|
||||
</div>
|
||||
|
||||
<div class="topbar-right">
|
||||
{% block topbar_right %}{% endblock %}
|
||||
<span class="timer-pill" id="global-timer-pill"
|
||||
data-last-run-ms="{{ global_last_run_epoch_ms or '' }}"
|
||||
data-interval-ms="{{ (global_check_interval * 60000) if global_check_interval else '' }}">
|
||||
<span class="dot"></span>Nächste Prüfung in <span id="global-timer">--</span>s
|
||||
</span>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<a href="{{ url_for('login') }}" class="btn btn-primary">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4"/><path d="M10 17l5-5-5-5"/><path d="M15 12H3"/></svg>
|
||||
|
||||
@@ -5,29 +5,37 @@
|
||||
{% 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="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Zugangsdaten</h2>
|
||||
<div class="hint">
|
||||
Zugangsdaten können mehreren Switchen gleichzeitig zugeordnet werden. Beim Anlegen eines
|
||||
Switches lassen sich bestehende Zugangsdaten auswählen oder direkt neue hinterlegen.
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<table class="data-table" data-sortable>
|
||||
<thead><tr>
|
||||
<th data-sort-key="name">Name</th>
|
||||
<th data-sort-key="username">Username</th>
|
||||
<th data-sort-key="usage">Verwendet von</th>
|
||||
<th style="width:1%;">Aktionen</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for c in credentials %}
|
||||
<tr>
|
||||
<tr data-sort-name="{{ c['name']|lower }}" data-sort-username="{{ c['username']|lower }}" data-sort-usage="{{ c['switch_count'] }}">
|
||||
<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>
|
||||
|
||||
@@ -8,17 +8,22 @@
|
||||
{% set col_count = 5 + (1 if can_toggle else 0) + (1 if show_actions_col else 0) %}
|
||||
{% block page_title %}Clients{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub">{{ devices|length }} Geräte</div>{% endblock %}
|
||||
{% block topbar_right %}
|
||||
{% if can_create %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="deviceModal" onclick="resetDeviceForm()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
Neues Gerät
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Geräte</h2>
|
||||
<div class="hint">PoE-Geräte mit IP, MAC-Adresse und zugeordnetem Switch-Port für die automatische Erreichbarkeitsprüfung.</div>
|
||||
</div>
|
||||
{% if can_create %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="deviceModal" onclick="resetDeviceForm()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
Neues Gerät
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<div class="table-toolbar">
|
||||
<div class="search-input">
|
||||
@@ -27,21 +32,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="data-table" id="devicesTable">
|
||||
<table class="data-table" id="devicesTable" data-sortable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>IP-Adresse</th>
|
||||
<th>MAC-Adresse</th>
|
||||
<th>Switch</th>
|
||||
<th>Port</th>
|
||||
<th data-sort-key="hostname">Hostname</th>
|
||||
<th data-sort-key="ip">IP-Adresse</th>
|
||||
<th data-sort-key="mac">MAC-Adresse</th>
|
||||
<th data-sort-key="switch">Switch</th>
|
||||
<th data-sort-key="port">Port</th>
|
||||
{% if can_toggle %}<th>Status</th>{% endif %}
|
||||
{% if show_actions_col %}<th style="width:1%;">Aktionen</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
<tr data-sort-hostname="{{ d['name']|lower }}" data-sort-ip="{{ d['rpi_ip']|lower }}" data-sort-mac="{{ d['mac']|lower }}" data-sort-switch="{{ (d['switch_hostname'] or '')|lower }}" data-sort-port="{{ (d['port'] or '')|lower }}">
|
||||
<td class="cell-name">{{ d['name'] }}</td>
|
||||
<td class="mono">{{ d['rpi_ip'] }}</td>
|
||||
<td class="mono">{{ d['mac'] }}</td>
|
||||
|
||||
@@ -2,28 +2,37 @@
|
||||
{% set active_page = "groups" %}
|
||||
{% block page_title %}Gruppen{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub">{{ groups|length + 1 }} Gruppen · Rechteverwaltung</div>{% endblock %}
|
||||
{% block topbar_right %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="addGroupModal">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
Neue Gruppe
|
||||
</button>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p class="text-faint" style="font-size:12.5px; margin-bottom:18px; max-width:720px;">
|
||||
Ü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>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Gruppen</h2>
|
||||
<div class="hint">
|
||||
Ü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.
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" data-open-modal="addGroupModal">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
Neue Gruppe
|
||||
</button>
|
||||
</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. -->
|
||||
<table class="data-table" data-sortable>
|
||||
<thead><tr>
|
||||
<th data-sort-key="name">Gruppe</th>
|
||||
<th data-sort-key="members">Mitglieder</th>
|
||||
<th style="width:1%;">Aktionen</th>
|
||||
</tr></thead>
|
||||
|
||||
<!-- Virtuelle "Admin"-Gruppe: Rechte sind fix (alles), Mitgliedschaft
|
||||
wird direkt über is_admin gesteuert. Bleibt beim Sortieren immer
|
||||
oben (eigenes <tbody data-sort-pinned>). -->
|
||||
<tbody data-sort-pinned>
|
||||
<tr>
|
||||
<td class="cell-name">Admin <span class="pill admin">Systemrolle</span></td>
|
||||
<td class="text-dim">{{ admin_virtual_group.member_names|length }}</td>
|
||||
@@ -58,8 +67,12 @@
|
||||
<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>
|
||||
</tbody>
|
||||
|
||||
{% for g in groups %}
|
||||
{% for g in groups %}
|
||||
<!-- Jede Gruppe (Haupt- + Detail-Zeile) in einem eigenen <tbody>, damit
|
||||
die Sortierung beide Zeilen gemeinsam verschiebt. -->
|
||||
<tbody data-sort-name="{{ g.name|lower }}" data-sort-members="{{ g.member_names|length }}">
|
||||
<tr>
|
||||
<td class="cell-name">
|
||||
{{ g.name }}
|
||||
@@ -116,10 +129,12 @@
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr class="empty-row"><td colspan="3">Noch keine weiteren Gruppen angelegt.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% else %}
|
||||
<tbody data-sort-pinned>
|
||||
<tr class="empty-row"><td colspan="3">Noch keine weiteren Gruppen angelegt.</td></tr>
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
{% set active_page = "index" %}
|
||||
{% block page_title %}Dashboard{% 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;">
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="search-input" style="max-width:320px; margin-bottom:18px;">
|
||||
<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 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="stat-card">
|
||||
@@ -169,28 +167,27 @@
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const intervalMinutes = {{ interval | int }};
|
||||
const intervalMinutes = {{ global_check_interval | int }};
|
||||
const intervalMilliseconds = intervalMinutes * 60 * 1000;
|
||||
const isAuthenticated = {{ "true" if current_user.is_authenticated else "false" }};
|
||||
// Vom Server ermittelter Zeitpunkt des letzten echten Prüf-Durchlaufs
|
||||
// (aus dem Logfile) — damit startet der Countdown nicht bei jedem
|
||||
// Seitenaufruf wieder von vorn, sondern zeigt die tatsächlich
|
||||
// verbleibende Zeit bis zur nächsten Prüfung durch poe.sh.
|
||||
const lastRunAt = {{ last_run_epoch_ms | tojson }};
|
||||
// verbleibende Zeit bis zur nächsten Prüfung durch poe.sh. Der
|
||||
// sichtbare Countdown selbst läuft global in der Topbar (app.js); hier
|
||||
// wird nur überwacht, wann ein neuer Durchlauf fertig ist, um die
|
||||
// Dashboard-Kacheln dann automatisch neu zu laden.
|
||||
const lastRunAt = {{ global_last_run_epoch_ms | tojson }};
|
||||
let lastUpdateTime = lastRunAt || Date.now();
|
||||
|
||||
function updateTimer() {
|
||||
function checkForReload() {
|
||||
const now = Date.now();
|
||||
const elapsed = now - lastUpdateTime;
|
||||
const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
|
||||
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||
const el = document.getElementById("dashboard-timer");
|
||||
if (el) el.innerText = `Nächste Prüfung in ${remainingSec}s`;
|
||||
if (remainingSec <= 1) window.location.reload();
|
||||
if (Math.ceil(remainingMs / 1000) <= 1) window.location.reload();
|
||||
}
|
||||
|
||||
setInterval(updateTimer, 1000);
|
||||
updateTimer();
|
||||
if (intervalMilliseconds) setInterval(checkForReload, 1000);
|
||||
|
||||
// Suchfilter über alle Abschnitte hinweg; Abschnitt wird komplett
|
||||
// ausgeblendet, wenn keine seiner Kacheln mehr passt.
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
{% set active_page = "logs" %}
|
||||
{% block page_title %}Live{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub">{{ log_name or "kein Logfile" }}</div>{% endblock %}
|
||||
{% block topbar_right %}
|
||||
<span class="timer-pill"><span class="dot"></span>Update in <span id="timer">--</span>s</span>
|
||||
<button id="refresh-btn" class="btn btn-secondary">
|
||||
<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>
|
||||
Aktualisieren
|
||||
</button>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Live-Log</h2>
|
||||
<div class="hint">Laufende Erreichbarkeitsprüfung von poe.sh, farblich markiert (online/offline).</div>
|
||||
</div>
|
||||
<button id="refresh-btn" class="btn btn-secondary">
|
||||
<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>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="log-shell">
|
||||
<div class="log-toolbar">
|
||||
<div class="log-dots"><span></span><span></span><span></span></div>
|
||||
@@ -22,13 +27,6 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function parseLogTimestamp(ts) {
|
||||
const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
||||
if (!parts) return Date.now();
|
||||
const [, year, month, day, hour, minute, second] = parts.map(Number);
|
||||
return new Date(year, month - 1, day, hour, minute, second).getTime();
|
||||
}
|
||||
|
||||
function colorizeLine(line) {
|
||||
let cls = "";
|
||||
if (line.includes(" ist erreichbar!")) cls = "online";
|
||||
@@ -42,9 +40,8 @@ function colorizeLine(line) {
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const intervalMinutes = {{ interval | int }};
|
||||
const intervalMinutes = {{ global_check_interval | int }};
|
||||
const intervalMilliseconds = intervalMinutes * 60 * 1000;
|
||||
let lastUpdateTime = Date.now();
|
||||
|
||||
function renderLog(data) {
|
||||
const box = document.getElementById("log-box");
|
||||
@@ -55,17 +52,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (i < lines.length - 1) box.appendChild(document.createElement("br"));
|
||||
});
|
||||
box.scrollTop = box.scrollHeight;
|
||||
|
||||
let lastSep = -1;
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
if (lines[i].startsWith("----")) { lastSep = i; break; }
|
||||
}
|
||||
if (lastSep >= 0 && lastSep + 1 < lines.length) {
|
||||
const match = lines[lastSep + 1].match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
|
||||
lastUpdateTime = match ? parseLogTimestamp(match[1]) : Date.now();
|
||||
} else {
|
||||
lastUpdateTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchLog() {
|
||||
@@ -75,17 +61,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
const now = Date.now();
|
||||
const elapsed = now - lastUpdateTime;
|
||||
const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
|
||||
document.getElementById("timer").innerText = Math.ceil(remainingMs / 1000);
|
||||
}
|
||||
|
||||
document.getElementById("refresh-btn").addEventListener("click", fetchLog);
|
||||
fetchLog();
|
||||
setInterval(fetchLog, intervalMilliseconds);
|
||||
setInterval(updateTimer, 1000);
|
||||
if (intervalMilliseconds) setInterval(fetchLog, intervalMilliseconds);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,17 +8,22 @@
|
||||
{% set can_delete = current_user.has_permission('switches.delete') %}
|
||||
{% block page_title %}Switche{% endblock %}
|
||||
{% 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" 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>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Switche</h2>
|
||||
<div class="hint">Aruba-Switche mit ihren Zugangsdaten, über die der automatische PoE-Neustart bei Ausfällen sowie der Verbindungstest laufen.</div>
|
||||
</div>
|
||||
{% if can_create %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<div class="table-toolbar">
|
||||
<div class="search-input">
|
||||
@@ -27,13 +32,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="data-table" id="switchesTable">
|
||||
<table class="data-table" id="switchesTable" data-sortable>
|
||||
<thead>
|
||||
<tr><th>Hostname</th><th>IP-Adresse</th><th>Zugangsdaten</th><th style="width:1%;">Aktionen</th></tr>
|
||||
<tr>
|
||||
<th data-sort-key="hostname">Hostname</th>
|
||||
<th data-sort-key="ip">IP-Adresse</th>
|
||||
<th data-sort-key="credential">Zugangsdaten</th>
|
||||
<th style="width:1%;">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in switches %}
|
||||
<tr>
|
||||
<tr data-sort-hostname="{{ s['hostname']|lower }}" data-sort-ip="{{ s['ip']|lower }}" data-sort-credential="{{ (s['credential_name'] or '')|lower }}">
|
||||
<td class="cell-name">{{ s['hostname'] }}</td>
|
||||
<td class="mono">{{ s['ip'] }}</td>
|
||||
<td>
|
||||
|
||||
@@ -2,26 +2,36 @@
|
||||
{% set active_page = "users" %}
|
||||
{% block page_title %}Benutzer{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub">{{ users|length }} Benutzer</div>{% endblock %}
|
||||
{% block topbar_right %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="userModal" onclick="document.getElementById('userForm').reset();">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
Neuer Benutzer
|
||||
</button>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Benutzer</h2>
|
||||
<div class="hint">Benutzerkonten mit Zugriff auf den PoE Manager. Die Gruppe bestimmt, welche Verwaltungsrechte ein Benutzer hat.</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" data-open-modal="userModal" onclick="document.getElementById('userForm').reset();">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
Neuer Benutzer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Username</th><th>Name</th><th>Gruppe</th><th style="width:1%;">Aktionen</th></tr></thead>
|
||||
<table class="data-table" data-sortable>
|
||||
<thead><tr>
|
||||
<th data-sort-key="username">Username</th>
|
||||
<th data-sort-key="name">Name</th>
|
||||
<th data-sort-key="group">Gruppe</th>
|
||||
<th style="width:1%;">Aktionen</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
{% set full_name = [u['first_name'], u['last_name']]|select|join(' ') %}
|
||||
{% set group_label = 'Admin' if u['is_admin'] else (u['group_names'] or '') %}
|
||||
<tr data-sort-username="{{ u['username']|lower }}" data-sort-name="{{ full_name|lower }}" data-sort-group="{{ group_label|lower }}">
|
||||
<td class="cell-name">{{ u['username'] }}</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 class="text-dim">{{ full_name or '—' }}</td>
|
||||
<td>
|
||||
{% if u['is_admin'] %}
|
||||
<span class="pill admin">Admin</span>
|
||||
|
||||
Reference in New Issue
Block a user