Dashboard-Kacheln, Ansichtsrechte, Standardgruppe, Countdown-Fix, Deploy-Pfad

- Dashboard zeigt jetzt in beiden Zustaenden (mit/ohne Login) Kacheln statt
  Tabelle; ohne Login nur aktive Geraete + Online/Offline/Gesamt, eingeloggt
  alle Geraete + zusaetzliche Deaktiviert-Kachel
- Sortierung ueberall: erst alle Nicht-Online-Geraete, dann Online, jeweils
  alphabetisch
- Neue Rechte devices.view / switches.view; Standardgruppe 'Benutzer' wird
  automatisch angelegt (alle Ansichtsrechte) und jedem neuen Benutzer
  zugeordnet; bestehende Benutzer ohne Gruppe werden migriert
- Gruppen-Seite zeigt zusaetzlich virtuelle 'Admin'-Karte (informativ) und
  markiert die Standardgruppe (nicht loeschbar)
- 'Naechste Pruefung'-Countdown wird serverseitig aus dem tatsaechlichen
  letzten Log-Eintrag geseedet statt bei jedem Reload neu zu starten
- Deployment-Doku korrigiert: Checkout getrennt von /srv/poe_manager, damit
  kein verschachteltes srv/poe_manager/srv/poe_manager entsteht

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 12:41:21 +02:00
co-authored by Claude Sonnet 5
parent 82bfeb17ed
commit 08d8d1f738
7 changed files with 241 additions and 101 deletions
+26 -73
View File
@@ -1,7 +1,7 @@
{% 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</div>{% endblock %}
{% block page_sub %}<div class="topbar-sub">{{ devices|length }} Geräte{{ " im Bestand" if current_user.is_authenticated else "" }}</div>{% endblock %}
{% block topbar_right %}
<span class="timer-pill"><span class="dot"></span><span id="dashboard-timer">Nächste Prüfung in --s</span></span>
{% endblock %}
@@ -17,62 +17,23 @@
<div class="stat-label">Offline</div>
<div class="stat-value offline">{{ stats.offline }}</div>
</div>
{% if current_user.is_authenticated %}
<div class="stat-card">
<div class="stat-label">Deaktiviert</div>
<div class="stat-value disabled">{{ stats.disabled }}</div>
</div>
{% endif %}
<div class="stat-card">
<div class="stat-label">Gesamt</div>
<div class="stat-value">{{ stats.total }}</div>
</div>
</div>
{% if not current_user.is_authenticated %}
{# ============================================================ #}
{# Öffentliche, schreibgeschützte Kurzübersicht (kein Login) #}
{# ============================================================ #}
<div class="table-wrap">
<div style="overflow-x:auto;">
<table class="data-table">
<thead>
<tr><th>Hostname</th><th>IP-Adresse</th><th>Status</th><th>Letzte Prüfung</th></tr>
</thead>
<tbody>
{% for d in devices %}
{% set st = status.get(d['mac'], 'unbekannt') %}
<tr>
<td class="cell-name">{{ d['name'] }}</td>
<td class="mono">{{ d['rpi_ip'] }}</td>
<td>
{% if d['is_active'] == 0 %}
<span class="pill disabled">Deaktiviert</span>
{% else %}
<span class="pill {{ st }}">{{ {'online':'Online','offline':'Offline','unbekannt':'Unbekannt'}[st] }}</span>
{% endif %}
</td>
<td class="text-dim">{{ last_checked.get(d['mac']) or '—' }}</td>
</tr>
{% else %}
<tr class="empty-row"><td colspan="4">Noch keine Geräte vorhanden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<p class="text-faint mt-3" style="font-size:12px;">
Nur Lese-Ansicht. <a href="{{ url_for('login') }}" style="color:var(--accent-strong); font-weight:600;">Anmelden</a>,
um Geräte zu verwalten oder einen PoE-Neustart auszulösen.
</p>
{% else %}
{# ============================================================ #}
{# Vollständiges Dashboard für eingeloggte Benutzer #}
{# ============================================================ #}
{% 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 %}"
<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'] }}"
@@ -99,10 +60,23 @@
</div>
{% else %}
<div class="card card-pad" style="text-align:center; color:var(--text-faint);">
{% if current_user.is_authenticated %}
Noch keine Geräte angelegt. Füge welche unter <a href="{{ url_for('devices') }}" style="color:var(--accent-strong); font-weight:600;">Devices</a> hinzu.
{% else %}
Aktuell keine Geräte verfügbar.
{% endif %}
</div>
{% endif %}
{% if not current_user.is_authenticated %}
<p class="text-faint mt-3" style="font-size:12px;">
Nur Lese-Ansicht — deaktivierte Geräte werden nicht angezeigt.
<a href="{{ url_for('login') }}" style="color:var(--accent-strong); font-weight:600;">Anmelden</a>,
um alle Geräte zu verwalten oder einen PoE-Neustart auszulösen.
</p>
{% endif %}
{% if current_user.is_authenticated %}
<!-- Device detail modal -->
<div class="modal-overlay" id="deviceModal">
<div class="modal">
@@ -159,7 +133,12 @@ document.addEventListener("DOMContentLoaded", () => {
const intervalMinutes = {{ interval | int }};
const intervalMilliseconds = intervalMinutes * 60 * 1000;
const isAuthenticated = {{ "true" if current_user.is_authenticated else "false" }};
let lastUpdateTime = Date.now();
// 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 }};
let lastUpdateTime = lastRunAt || Date.now();
function updateTimer() {
const now = Date.now();
@@ -171,38 +150,15 @@ document.addEventListener("DOMContentLoaded", () => {
if (remainingSec <= 1) window.location.reload();
}
setInterval(updateTimer, 1000);
updateTimer();
if (!isAuthenticated) {
// Öffentliche Ansicht: einfacher Reload-Timer, kein Zugriff auf /get_log nötig.
setInterval(updateTimer, 1000);
return;
}
let selectedCard = null, selectedMac = null, selectedName = null;
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 fetchLastLog() {
fetch("{{ url_for('get_log') }}")
.then(r => r.text())
.then(data => {
const lines = data.split("\n");
let lastSepIndex = -1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("----")) { lastSepIndex = i; break; }
}
if (lastSepIndex >= 0 && lastSepIndex + 1 < lines.length) {
const match = lines[lastSepIndex + 1].match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
if (match) lastUpdateTime = parseLogTimestamp(match[1]);
}
})
.catch(() => {});
}
document.querySelectorAll(".device-card").forEach(card => {
const mac = card.dataset.mac;
const restartKey = "restart_" + mac;
@@ -263,9 +219,6 @@ document.addEventListener("DOMContentLoaded", () => {
.catch(err => { button.disabled = false; showToast("Fehler beim Starten des Neustarts.", "danger"); });
});
}
setInterval(updateTimer, 1000);
fetchLastLog();
});
</script>
{% endblock %}