UI-Fixes (Button-Wrap, Suchfeld-Icon, Log-Farben, Nav-Persistenz), manueller Check-Trigger, echter Live-Refresh
- section-head: Button ("+ Neue Zugangsdaten" etc.) rutscht bei langer
Beschreibung nicht mehr in die nächste Zeile — bleibt immer rechts oben,
der Beschreibungstext wickelt stattdessen innerhalb seiner eigenen Spalte.
- Suchfeld-Icon überlappte den Platzhaltertext: eine spätere, generische
Input-Regel mit gleicher Spezifität hat die padding-left-Regel des
Lupe-Icons überschrieben (reine Reihenfolge-im-Stylesheet-Sache) — Selektor
spezifischer gemacht.
- Live-Log: Neustart-bezogene Zeilen (manueller Neustart, automatischer
PoE-Restart bei Ausfall) werden jetzt orange markiert statt farblos.
- Aufgeklappte Nav-Gruppen bleiben über Seitenwechsel hinweg erhalten
(localStorage), zusätzlich zur automatischen Aufklappung der Gruppe der
aktuell aktiven Seite.
- Datei-Auswahl (Import) im Dark Mode gestylt (Text + Button), inkl.
color-scheme-bewusster Browser-Widgets.
- Manueller "Jetzt prüfen"-Trigger (Icon neben dem globalen Timer, nur
Admins): startet rpi-check.service neu -> frisches Logfile + sofortiger
Durchlauf, Countdown synchronisiert sich auf den manuellen Zeitpunkt.
- Dashboard und Live-Log aktualisieren sich jetzt per AJAX im Hintergrund
(kein voller Seiten-Reload mehr): neue Route /dashboard/tiles liefert das
Kachel-Fragment (_dashboard_tiles.html, von index.html eingebunden und vom
Live-Update nachgeladen), get_log() liefert den aktuellen Dateinamen als
Header mit, damit die Anzeige nach einem Service-Neustart aktuell bleibt.
- Root-Cause für "Intervall bleibt immer bei 300s": in der WSL-Testumgebung
lief poe_wrapper.py nur als manuell gestarteter Hintergrundprozess statt
als echter rpi-check.service — "systemctl restart" lief dadurch ins Leere.
Echten systemd-Service in WSL eingerichtet und Intervalländerung end-to-end
verifiziert (Log-Zeilenabstand folgt jetzt dem neu gesetzten Intervall).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,94 +10,7 @@
|
||||
<input type="text" id="tileSearch" placeholder="Gerät suchen…">
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Online</div>
|
||||
<div class="stat-value online">{{ stats.online }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<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>
|
||||
|
||||
{% 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="{{ 'disabled' 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">Deaktiviert</span>
|
||||
{% else %}
|
||||
<span class="pill {{ st }}">{{ {'online':'Online','offline':'Offline','unbekannt':'Unbekannt'}[st] }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<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 %}
|
||||
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 %}
|
||||
{% include "_dashboard_tiles.html" %}
|
||||
|
||||
{% if not current_user.is_authenticated %}
|
||||
<p class="text-faint mt-3" style="font-size:12px;">
|
||||
@@ -167,88 +80,122 @@
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
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. 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 checkForReload() {
|
||||
const now = Date.now();
|
||||
const elapsed = now - lastUpdateTime;
|
||||
const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
|
||||
if (Math.ceil(remainingMs / 1000) <= 1) window.location.reload();
|
||||
}
|
||||
|
||||
if (intervalMilliseconds) setInterval(checkForReload, 1000);
|
||||
let selectedCard = null, selectedMac = null, selectedName = null;
|
||||
const restartButton = document.getElementById("restartButton");
|
||||
const activateButton = document.getElementById("activateButton");
|
||||
const searchInput = document.getElementById("tileSearch");
|
||||
|
||||
// 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;
|
||||
// ausgeblendet, wenn keine seiner Kacheln mehr passt. Wird nach jedem
|
||||
// Live-Update erneut angewendet, damit ein aktiver Filter erhalten bleibt.
|
||||
function applySearchFilter(query) {
|
||||
const q = (query || "").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 (searchInput) searchInput.addEventListener("input", () => applySearchFilter(searchInput.value));
|
||||
|
||||
// Klick-Verhalten der Kacheln (Detail-Modal, Neustart/Aktivieren) — wird
|
||||
// nach jedem Live-Update erneut auf die frisch eingefügten Kacheln
|
||||
// angewendet, da deren alte DOM-Knoten beim Austausch verworfen werden.
|
||||
function bindDeviceCards() {
|
||||
if (!isAuthenticated) return;
|
||||
document.querySelectorAll(".device-card").forEach(card => {
|
||||
const mac = card.dataset.mac;
|
||||
const restartKey = "restart_" + mac;
|
||||
if (sessionStorage.getItem(restartKey)) {
|
||||
const pill = card.querySelector(".pill");
|
||||
if (pill && pill.innerText.trim().toLowerCase() === "online") {
|
||||
sessionStorage.removeItem(restartKey);
|
||||
} else if (pill) {
|
||||
pill.innerHTML = '<span class="spinner"></span> Restarting…';
|
||||
}
|
||||
}
|
||||
card.addEventListener("click", function () {
|
||||
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 || "-";
|
||||
document.getElementById("devicePort").innerText = this.dataset.port || "-";
|
||||
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 noResults = document.getElementById("noSearchResults");
|
||||
if (noResults) noResults.classList.toggle("hidden", anyVisible || q === "");
|
||||
});
|
||||
}
|
||||
bindDeviceCards();
|
||||
|
||||
// Live-Update der Dashboard-Kacheln ohne vollen Seiten-Reload: tauscht
|
||||
// nur den #dashboard-tiles-Container gegen das AJAX-Partial aus (siehe
|
||||
// /dashboard/tiles) und hält dabei Suchfilter, Scroll-Position und
|
||||
// Sidebar-Zustand unangetastet. Läuft für an- und abgemeldete Ansicht.
|
||||
let refreshTimer = null;
|
||||
|
||||
function scheduleNextRefresh(overrideMs) {
|
||||
if (refreshTimer) clearTimeout(refreshTimer);
|
||||
if (overrideMs) { refreshTimer = setTimeout(refreshDashboardTiles, overrideMs); return; }
|
||||
const pill = document.getElementById("global-timer-pill");
|
||||
const intervalMs = pill ? parseInt(pill.dataset.intervalMs, 10) : NaN;
|
||||
if (!intervalMs) return;
|
||||
const lastRunMs = parseInt(pill.dataset.lastRunMs, 10);
|
||||
// + kleiner Puffer, bis poe.sh die neue Zeile tatsächlich geschrieben hat.
|
||||
let waitMs = intervalMs + 1500;
|
||||
if (!isNaN(lastRunMs) && lastRunMs > 0) {
|
||||
const elapsed = Date.now() - lastRunMs;
|
||||
waitMs = (intervalMs - (((elapsed % intervalMs) + intervalMs) % intervalMs)) + 1500;
|
||||
}
|
||||
refreshTimer = setTimeout(refreshDashboardTiles, waitMs);
|
||||
}
|
||||
|
||||
function refreshDashboardTiles() {
|
||||
fetch("{{ url_for('dashboard_tiles') }}")
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
const container = document.getElementById("dashboard-tiles");
|
||||
if (!container) return;
|
||||
const temp = document.createElement("div");
|
||||
temp.innerHTML = html;
|
||||
const fresh = temp.querySelector("#dashboard-tiles");
|
||||
if (!fresh) return;
|
||||
container.replaceWith(fresh);
|
||||
// Globalen Topbar-Timer mit dem tatsächlichen Zeitpunkt
|
||||
// dieses Durchlaufs synchronisieren (initCheckTimer() in
|
||||
// app.js liest das Attribut bei jedem Tick neu ein).
|
||||
const pill = document.getElementById("global-timer-pill");
|
||||
if (pill) pill.dataset.lastRunMs = fresh.dataset.lastRunMs || "";
|
||||
bindDeviceCards();
|
||||
if (searchInput) applySearchFilter(searchInput.value);
|
||||
scheduleNextRefresh();
|
||||
})
|
||||
.catch(() => scheduleNextRefresh(15000));
|
||||
}
|
||||
|
||||
scheduleNextRefresh();
|
||||
document.addEventListener("poe:check-triggered", refreshDashboardTiles);
|
||||
|
||||
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;
|
||||
const restartKey = "restart_" + mac;
|
||||
if (sessionStorage.getItem(restartKey)) {
|
||||
const pill = card.querySelector(".pill");
|
||||
if (pill && pill.innerText.trim().toLowerCase() === "online") {
|
||||
sessionStorage.removeItem(restartKey);
|
||||
} else if (pill) {
|
||||
pill.innerHTML = '<span class="spinner"></span> Restarting…';
|
||||
}
|
||||
}
|
||||
card.addEventListener("click", function () {
|
||||
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 || "-";
|
||||
document.getElementById("devicePort").innerText = this.dataset.port || "-";
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
if (restartButton) {
|
||||
restartButton.addEventListener("click", function () {
|
||||
PoeUI.closeModal(document.getElementById("deviceModal"));
|
||||
@@ -295,7 +242,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
PoeUI.closeModal(document.getElementById("deviceModal"));
|
||||
showToast(data.msg, "success");
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
refreshDashboardTiles();
|
||||
})
|
||||
.catch(() => { button.disabled = false; showToast("Fehler beim Aktivieren.", "danger"); });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user