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:
@@ -31,7 +31,11 @@ Die App ermöglicht:
|
||||
der Countdown „Nächste Prüfung“ wird aus dem tatsächlichen letzten
|
||||
Log-Eintrag berechnet, nicht bei jedem Seitenaufruf neu gestartet, und
|
||||
läuft **global in der Topbar auf jeder Seite** (nicht nur im Dashboard)
|
||||
- Live-Log-Ansicht mit farblicher Hervorhebung (online/offline)
|
||||
- Live-Log-Ansicht mit farblicher Hervorhebung (online/offline/Neustart);
|
||||
Dashboard und Live-Log aktualisieren sich automatisch im Hintergrund
|
||||
(per AJAX, ohne vollen Seiten-Reload). Ein „Jetzt prüfen"-Icon neben dem
|
||||
Timer (nur Admins) startet manuell einen sofortigen Prüf-Durchlauf —
|
||||
der Countdown setzt sich dabei auf den manuell ausgelösten Zeitpunkt zurück
|
||||
- **Einheitliches Seiten-Layout**: die Topbar zeigt rechts ausschließlich
|
||||
den „Nächste Prüfung“-Timer; Anlegen-Buttons und eine kurze Beschreibung
|
||||
sitzen stattdessen direkt über der jeweiligen Tabelle. Die Listen
|
||||
@@ -241,6 +245,19 @@ python3 -m venv venv
|
||||
# auch aus dem Windows-Browser erreichbar
|
||||
```
|
||||
|
||||
**Hinweis zu `rpi-check.service`:** Der „Speichern & Service neustarten“-Button
|
||||
unter Systemeinstellungen sowie das „Jetzt prüfen“-Icon funktionieren nur,
|
||||
wenn `rpi-check.service` tatsächlich als echter systemd-Service läuft (viele
|
||||
aktuelle WSL2-Installationen unterstützen systemd, siehe `wsl.conf`
|
||||
`[boot] systemd=true`). Für lokale Tests reicht es, `etc/systemd/system/
|
||||
rpi-check.service` sowie `usr/local/bin/custom/poe.sh` per Symlink an ihre
|
||||
Produktivpfade (`/etc/systemd/system/`, `/usr/local/bin/custom/`) zu legen
|
||||
und `/srv/poe_manager` auf den Checkout zu verlinken, dann `systemctl
|
||||
daemon-reload && systemctl enable --now rpi-check.service`. Ohne systemd
|
||||
(z.B. `systemctl` nicht vorhanden) scheitert der Neustart-Aufruf lautlos —
|
||||
das Intervall in der DB wird trotzdem aktualisiert, greift dann aber erst
|
||||
nach einem manuellen Neustart des Prozesses.
|
||||
|
||||
## Lokal starten (Entwicklung)
|
||||
|
||||
```bash
|
||||
|
||||
+62
-19
@@ -740,16 +740,10 @@ def get_device_status(devices):
|
||||
# Dashboard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""
|
||||
Dashboard als Kachel-Ansicht, in drei Abschnitten: Offline, Online,
|
||||
Deaktiviert (jeweils alphabetisch) — jeweils nur gerendert, wenn nicht
|
||||
leer. Ohne Login: keine deaktivierten Geräte, nur Online/Offline/Gesamt-
|
||||
Statistik, keine Interaktion. Eingeloggt: alle Geräte, volle
|
||||
Statistik-Kacheln, Klick-Details, manueller PoE-Neustart bzw. bei
|
||||
deaktivierten Geräten eine Aktivieren-Option.
|
||||
"""
|
||||
def _dashboard_context():
|
||||
"""Gemeinsam von index() und dem AJAX-Partial /dashboard/tiles genutzt,
|
||||
damit das Live-Update des Dashboards (ohne vollen Seiten-Reload) exakt
|
||||
dieselben Daten liefert wie der initiale Seitenaufruf."""
|
||||
conn = get_db_connection()
|
||||
all_devices = conn.execute(
|
||||
"SELECT mac, name, rpi_ip, switch_hostname, port, is_active FROM devices ORDER BY name ASC"
|
||||
@@ -781,8 +775,7 @@ def index():
|
||||
|
||||
last_run_epoch_ms = int(last_run_at.timestamp() * 1000) if last_run_at else None
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
return dict(
|
||||
offline_devices=offline_devices, online_devices=online_devices, disabled_devices=disabled_devices,
|
||||
device_count=len(visible_devices),
|
||||
status=status, last_seen=last_seen,
|
||||
@@ -791,6 +784,54 @@ def index():
|
||||
)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""
|
||||
Dashboard als Kachel-Ansicht, in drei Abschnitten: Offline, Online,
|
||||
Deaktiviert (jeweils alphabetisch) — jeweils nur gerendert, wenn nicht
|
||||
leer. Ohne Login: keine deaktivierten Geräte, nur Online/Offline/Gesamt-
|
||||
Statistik, keine Interaktion. Eingeloggt: alle Geräte, volle
|
||||
Statistik-Kacheln, Klick-Details, manueller PoE-Neustart bzw. bei
|
||||
deaktivierten Geräten eine Aktivieren-Option.
|
||||
"""
|
||||
return render_template("index.html", **_dashboard_context())
|
||||
|
||||
|
||||
@app.route("/dashboard/tiles")
|
||||
def dashboard_tiles():
|
||||
"""AJAX-Partial für das Live-Update der Dashboard-Kacheln (Status,
|
||||
Statistik) ohne vollen Seiten-Reload — liefert dasselbe Fragment, das
|
||||
index.html beim initialen Aufruf einbindet (siehe _dashboard_tiles.html)."""
|
||||
return render_template("_dashboard_tiles.html", **_dashboard_context())
|
||||
|
||||
|
||||
def _restart_check_service():
|
||||
"""Startet rpi-check.service (poe_wrapper.py -> poe.sh) neu, damit ein
|
||||
geändertes Prüfintervall sofort greift (poe.sh liest SLEEP nur einmal
|
||||
beim Start aus der DB) — läuft dabei automatisch auch ein sofortiger
|
||||
Prüf-Durchlauf mit frischem Logfile an. Schlägt in Umgebungen ohne
|
||||
systemd (z.B. Container) lautlos fehl, statt die Anfrage abzubrechen."""
|
||||
try:
|
||||
subprocess.run(["systemctl", "restart", "rpi-check.service"], check=False)
|
||||
except FileNotFoundError:
|
||||
pass # z.B. lokale Entwicklungsumgebung ohne systemd
|
||||
|
||||
|
||||
@app.route("/check/run-now", methods=["POST"])
|
||||
@login_required
|
||||
def run_check_now():
|
||||
"""Manueller 'Jetzt prüfen'-Trigger (Zahnrad-Icon neben dem Prüf-Timer):
|
||||
startet denselben Service neu wie eine Intervalländerung — das erzeugt
|
||||
sofort ein frisches Logfile mit einem neuen Durchlauf, der Countdown
|
||||
'Nächste Prüfung' setzt sich dadurch automatisch auf den manuell
|
||||
ausgelösten Zeitpunkt zurück."""
|
||||
if not current_user.is_admin:
|
||||
return {"success": False, "msg": "Keine Berechtigung."}, 403
|
||||
_restart_check_service()
|
||||
log_action("check.run_now", "Prüfintervall")
|
||||
return {"success": True, "msg": "Prüfung gestartet."}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Settings
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -809,11 +850,7 @@ def settings():
|
||||
set_setting("interval", new_interval)
|
||||
set_setting("check_interval", new_interval * 60)
|
||||
log_action("settings.update", "Prüfintervall", f"{new_interval} Minuten")
|
||||
|
||||
try:
|
||||
subprocess.run(["systemctl", "restart", "rpi-check.service"], check=False)
|
||||
except FileNotFoundError:
|
||||
pass # z.B. lokale Entwicklungsumgebung ohne systemd
|
||||
_restart_check_service()
|
||||
|
||||
flash(f"Intervall auf {new_interval} Minuten gesetzt und Service neu gestartet!", "success")
|
||||
return redirect(url_for("settings"))
|
||||
@@ -1594,9 +1631,15 @@ def get_log():
|
||||
return "Keine Logfiles gefunden."
|
||||
try:
|
||||
with open(latest_log, "r") as f:
|
||||
return f.read()
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
return f"Fehler beim Lesen des Logs: {e}"
|
||||
content = f"Fehler beim Lesen des Logs: {e}"
|
||||
# Dateiname als Header mitschicken, damit die Anzeige auch nach einem
|
||||
# Service-Neustart (neues Logfile, z.B. durch "Jetzt prüfen") ohne
|
||||
# Seiten-Reload aktuell bleibt.
|
||||
response = app.response_class(content, mimetype="text/plain")
|
||||
response.headers["X-Log-Name"] = os.path.basename(latest_log)
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/logs")
|
||||
|
||||
@@ -429,12 +429,16 @@ button { font-family: inherit; }
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* Beschreibungstext darf mehrzeilig umbrechen, ohne dass der Button
|
||||
(z.B. "+ Neue Zugangsdaten") mit in die nächste Zeile rutscht — der
|
||||
bleibt immer rechts oben in derselben Zeile. */
|
||||
.section-head > div:first-child { flex: 1 1 auto; min-width: 0; }
|
||||
.section-head > .btn { flex-shrink: 0; }
|
||||
|
||||
.section-head h2 { font-size: 20px; }
|
||||
.section-head .hint { color: var(--text-faint); font-size: 12.5px; margin-top: 3px; }
|
||||
@@ -529,6 +533,22 @@ button { font-family: inherit; }
|
||||
100% { box-shadow: 0 0 0 0 rgba(47,208,122,0); }
|
||||
}
|
||||
|
||||
.timer-pill-refresh {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 18px; height: 18px;
|
||||
margin-left: 2px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.timer-pill-refresh svg { width: 12px; height: 12px; }
|
||||
.timer-pill-refresh:hover { color: var(--accent-strong); background: var(--accent-dim); }
|
||||
.timer-pill-refresh.spinning svg { animation: spin 0.8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ==========================================================================
|
||||
Device grid (dashboard)
|
||||
========================================================================== */
|
||||
@@ -663,7 +683,7 @@ button { font-family: inherit; }
|
||||
color: var(--text-faint);
|
||||
pointer-events: none;
|
||||
}
|
||||
.search-input input { padding-left: 34px; }
|
||||
.search-input input[type="text"] { padding-left: 34px; }
|
||||
|
||||
table.data-table {
|
||||
width: 100%;
|
||||
@@ -753,6 +773,40 @@ input[type="text"], input[type="password"], input[type="number"], input[type="em
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
input::placeholder { color: var(--text-faint); }
|
||||
|
||||
/* Datei-Auswahl (z.B. Import/Export) — der native Button/"keine Datei
|
||||
ausgewählt"-Text folgt sonst nicht dem Farbschema und ist im Dark Mode
|
||||
kaum lesbar (dunkler Text auf dunklem Grund). */
|
||||
input[type="file"] {
|
||||
width: 100%;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 9px;
|
||||
font-size: 13px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
input[type="file"]::file-selector-button,
|
||||
input[type="file"]::-webkit-file-upload-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--bg-card-hover);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
padding: 7px 12px;
|
||||
margin-right: 10px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
input[type="file"]::file-selector-button:hover,
|
||||
input[type="file"]::-webkit-file-upload-button:hover {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--text-faint);
|
||||
}
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
@@ -974,7 +1028,7 @@ select {
|
||||
.log-line.online { color: #4ade80; }
|
||||
.log-line.offline { color: #f87171; }
|
||||
.log-line.sep { color: #3a4050; }
|
||||
.log-line.manual { color: #ffb454; }
|
||||
.log-line.restart { color: #ffb454; }
|
||||
|
||||
/* ==========================================================================
|
||||
Permission checklists (Gruppen)
|
||||
|
||||
@@ -206,11 +206,39 @@
|
||||
} catch (e) { /* noop */ }
|
||||
}
|
||||
|
||||
/* Nav-Gruppen mit Untermenü (z.B. "Logs") auf-/zuklappen */
|
||||
/* Nav-Gruppen mit Untermenü (z.B. "Logs") auf-/zuklappen — der manuell
|
||||
aufgeklappte Zustand bleibt über Seitenwechsel hinweg erhalten
|
||||
(localStorage), zusätzlich zur automatischen Aufklappung der Gruppe
|
||||
der aktuell aktiven Seite (server-seitig via "expanded"-Klasse). */
|
||||
const NAV_EXPANDED_KEY = "poe-nav-expanded";
|
||||
|
||||
function getExpandedNavGroups() {
|
||||
try {
|
||||
return new Set(JSON.parse(localStorage.getItem(NAV_EXPANDED_KEY) || "[]"));
|
||||
} catch (e) {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function saveExpandedNavGroups(set) {
|
||||
localStorage.setItem(NAV_EXPANDED_KEY, JSON.stringify(Array.from(set)));
|
||||
}
|
||||
|
||||
function initNavGroups() {
|
||||
const expanded = getExpandedNavGroups();
|
||||
document.querySelectorAll("[data-nav-group]").forEach((group) => {
|
||||
const key = group.dataset.navGroupKey;
|
||||
if (key && expanded.has(key)) group.classList.add("expanded");
|
||||
});
|
||||
document.querySelectorAll("[data-nav-group-toggle]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
btn.closest("[data-nav-group]").classList.toggle("expanded");
|
||||
const group = btn.closest("[data-nav-group]");
|
||||
const isExpanded = group.classList.toggle("expanded");
|
||||
const key = group.dataset.navGroupKey;
|
||||
if (!key) return;
|
||||
const set = getExpandedNavGroups();
|
||||
if (isExpanded) set.add(key); else set.delete(key);
|
||||
saveExpandedNavGroups(set);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -221,11 +249,16 @@
|
||||
const pill = document.getElementById("global-timer-pill");
|
||||
const timerEl = document.getElementById("global-timer");
|
||||
if (!pill || !timerEl) return;
|
||||
const lastRunMs = parseInt(pill.dataset.lastRunMs, 10);
|
||||
const intervalMs = parseInt(pill.dataset.intervalMs, 10);
|
||||
if (!intervalMs) { timerEl.textContent = "--"; return; }
|
||||
|
||||
// lastRunMs/intervalMs werden bei JEDEM Tick frisch aus dem DOM gelesen
|
||||
// (nicht einmalig gecacht) — so kann z.B. das Dashboard nach einem
|
||||
// Live-Update (siehe dashboard-tiles.js) oder ein manueller "Jetzt
|
||||
// prüfen"-Klick das data-last-run-ms-Attribut aktualisieren und der
|
||||
// global laufende Countdown übernimmt das automatisch.
|
||||
function update() {
|
||||
const lastRunMs = parseInt(pill.dataset.lastRunMs, 10);
|
||||
const intervalMs = parseInt(pill.dataset.intervalMs, 10);
|
||||
if (!intervalMs) { timerEl.textContent = "--"; return; }
|
||||
let remainingMs = intervalMs;
|
||||
if (!isNaN(lastRunMs) && lastRunMs > 0) {
|
||||
const elapsed = Date.now() - lastRunMs;
|
||||
@@ -237,6 +270,42 @@
|
||||
setInterval(update, 1000);
|
||||
}
|
||||
|
||||
/* "Jetzt prüfen"-Button neben dem globalen Timer (nur Admins): startet
|
||||
rpi-check.service neu (frisches Logfile + sofortiger Durchlauf) und
|
||||
meldet das per Custom Event, damit Dashboard/Live-Log sich selbst
|
||||
aktualisieren können, ohne dass app.js die Seiteninhalte kennen muss. */
|
||||
function initRunCheckNow() {
|
||||
const btn = document.getElementById("run-check-now");
|
||||
if (!btn) return;
|
||||
btn.addEventListener("click", () => {
|
||||
btn.disabled = true;
|
||||
btn.classList.add("spinning");
|
||||
fetch("/check/run-now", { method: "POST" })
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (!data.success) {
|
||||
window.showToast(data.msg || "Fehler beim Starten der Prüfung.", "danger");
|
||||
btn.disabled = false;
|
||||
btn.classList.remove("spinning");
|
||||
return;
|
||||
}
|
||||
window.showToast(data.msg || "Prüfung gestartet.", "success");
|
||||
// Der Service braucht einen Moment, um neu zu starten und den
|
||||
// ersten Durchlauf ins frische Logfile zu schreiben.
|
||||
setTimeout(() => {
|
||||
document.dispatchEvent(new CustomEvent("poe:check-triggered"));
|
||||
btn.disabled = false;
|
||||
btn.classList.remove("spinning");
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
window.showToast("Fehler beim Starten der Prüfung.", "danger");
|
||||
btn.disabled = false;
|
||||
btn.classList.remove("spinning");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- Sortierbare Tabellen ---------------- */
|
||||
/*
|
||||
* Klick auf <th data-sort-key="..."> sortiert die Tabelle. Zwei Modi:
|
||||
@@ -300,6 +369,7 @@
|
||||
initFlashedMessages();
|
||||
initNavGroups();
|
||||
initCheckTimer();
|
||||
initRunCheckNow();
|
||||
initSortableTables();
|
||||
document.querySelectorAll("[data-theme-toggle]").forEach((btn) => btn.addEventListener("click", toggleTheme));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<!-- Live-Update-fähiges Fragment: wird von index.html initial eingebunden UND
|
||||
unverändert von /dashboard/tiles (AJAX) nachgeladen, damit das Dashboard
|
||||
ohne vollen Seiten-Reload aktuell bleibt (siehe dashboard-tiles.js im
|
||||
scripts-Block von index.html). -->
|
||||
<div id="dashboard-tiles" data-last-run-ms="{{ last_run_epoch_ms or '' }}">
|
||||
|
||||
<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 %}
|
||||
|
||||
</div>
|
||||
@@ -28,6 +28,7 @@
|
||||
"group.assign_admins": "Admin-Zuweisung geändert",
|
||||
"profile.update": "Profil aktualisiert",
|
||||
"profile.password": "Passwort geändert",
|
||||
"check.run_now": "Prüfung manuell gestartet",
|
||||
} %}
|
||||
{% set action_icons = {
|
||||
"delete": '<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"/>',
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
{% for item in nav_items_ordered %}
|
||||
{% if item.children %}
|
||||
{% set child_active = item.children|selectattr('endpoint', 'equalto', request.endpoint)|list %}
|
||||
<div class="nav-group {% if child_active %}expanded active-group{% endif %}" data-nav-group>
|
||||
<div class="nav-group {% if child_active %}expanded active-group{% endif %}" data-nav-group data-nav-group-key="{{ item.key }}">
|
||||
<button type="button" class="nav-group-toggle" data-nav-group-toggle>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ icons[item.icon]|safe }}</svg>
|
||||
{{ item.label }}
|
||||
@@ -116,6 +116,11 @@
|
||||
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
|
||||
{% if current_user.is_admin %}
|
||||
<button type="button" class="timer-pill-refresh" id="run-check-now" title="Jetzt prüfen">
|
||||
<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>
|
||||
</button>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<a href="{{ url_for('login') }}" class="btn btn-primary">
|
||||
|
||||
@@ -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"); });
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "logs" %}
|
||||
{% block page_title %}Live{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub">{{ log_name or "kein Logfile" }}</div>{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub" data-log-name>{{ log_name or "kein Logfile" }}</div>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="log-shell">
|
||||
<div class="log-toolbar">
|
||||
<div class="log-dots"><span></span><span></span><span></span></div>
|
||||
<span class="text-faint mono" style="font-size:11.5px;">{{ log_name or "" }}</span>
|
||||
<span class="text-faint mono" style="font-size:11.5px;" data-log-name>{{ log_name or "" }}</span>
|
||||
</div>
|
||||
<div id="log-box">{{ log_content or "Keine Logfiles gefunden." }}</div>
|
||||
</div>
|
||||
@@ -32,7 +32,9 @@ function colorizeLine(line) {
|
||||
if (line.includes(" ist erreichbar!")) cls = "online";
|
||||
else if (line.includes(" ist nicht erreichbar!")) cls = "offline";
|
||||
else if (line.startsWith("----")) cls = "sep";
|
||||
else if (line.toLowerCase().includes("manuell")) cls = "manual";
|
||||
// Neustart-bezogene Zeilen (manueller Neustart, automatischer PoE-Restart
|
||||
// bei Ausfall) einheitlich orange markieren.
|
||||
else if (line.toLowerCase().includes("manuell") || line.includes("PoE")) cls = "restart";
|
||||
const span = document.createElement("span");
|
||||
span.className = "log-line" + (cls ? " " + cls : "");
|
||||
span.textContent = line;
|
||||
@@ -43,25 +45,30 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const intervalMinutes = {{ global_check_interval | int }};
|
||||
const intervalMilliseconds = intervalMinutes * 60 * 1000;
|
||||
|
||||
function renderLog(data) {
|
||||
function renderLog(text, logName) {
|
||||
const box = document.getElementById("log-box");
|
||||
box.innerHTML = "";
|
||||
const lines = data.split("\n");
|
||||
const lines = text.split("\n");
|
||||
lines.forEach((line, i) => {
|
||||
box.appendChild(colorizeLine(line));
|
||||
if (i < lines.length - 1) box.appendChild(document.createElement("br"));
|
||||
});
|
||||
box.scrollTop = box.scrollHeight;
|
||||
// Dateiname mit anzeigen — ändert sich nach einem Service-Neustart
|
||||
// (neues Logfile, z.B. durch "Jetzt prüfen") ohne Seiten-Reload.
|
||||
if (logName) {
|
||||
document.querySelectorAll("[data-log-name]").forEach((el) => { el.textContent = logName; });
|
||||
}
|
||||
}
|
||||
|
||||
function fetchLog() {
|
||||
fetch("{{ url_for('get_log') }}")
|
||||
.then(r => r.text())
|
||||
.then(renderLog)
|
||||
.then(r => r.text().then((text) => renderLog(text, r.headers.get("X-Log-Name"))))
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
document.getElementById("refresh-btn").addEventListener("click", fetchLog);
|
||||
document.addEventListener("poe:check-triggered", fetchLog);
|
||||
fetchLog();
|
||||
if (intervalMilliseconds) setInterval(fetchLog, intervalMilliseconds);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user