Logging & Log-Rotation, Zeitzone, Dashboard-UX, Design-Politur
Logging-Infrastruktur (Logs können nach einem Import mit vielen Geräten schnell vollaufen): - Vier eigene, rotierte Log-Dateien statt allem in /var/log vermischt: Live (poe.sh, jetzt eine einzige durchgehend fortgeschriebene Datei statt vorher ein neues rpi-<timestamp>.log pro Dienst-Neustart), Änderungen (Spiegel der audit_log-Tabelle), App (Flask-Fehler/ Warnungen, vorher nur im journalctl-Output), Kea-DHCP (eigene Logging-Konfiguration in der generierten kea-dhcp4.conf) - Kea-DHCP-Log bewusst NICHT im gemeinsamen Verzeichnis: Keas AppArmor-Profil erlaubt Schreibzugriff exklusiv auf /var/log/kea/kea-dhcp4.log (kein Verzeichnis-Wildcard) -- live als "DENIED ... mknod ... .lock" reproduziert; die App nutzt Keas eigenen, bereits erlaubten Pfad statt das AppArmor-Profil eines Systemdienstes aufzuweichen. logrotate deckt trotzdem beide Verzeichnisse mit einer Regel ab - Neue Log-Seite "Kea-DHCP" (Logs-Bereich, eigenes Recht logs_kea.view) - Rotation (Intervall täglich/wöchentlich/monatlich, Aufbewahrung als Anzahl Rotationen) unter Systemeinstellungen → Logs einstellbar, generiert /etc/logrotate.d/poe-manager; copytruncate, da keiner der drei schreibenden Prozesse ein Reopen-Signal implementiert - Einmalige Migration räumt alte, verwaiste rpi-*.log-Dateien auf Zeitzone (Systemeinstellungen, mit Hostname in einer Kachel "Host"): setzt die System-Zeitzone per timedatectl, damit Logs/Änderungsverlauf die tatsächliche lokale Zeit statt UTC zeigen. time.tzset() direkt nach dem Setzen lässt die bereits laufende poe_web.service-Instanz die neue Zeitzone sofort übernehmen, ohne Neustart. Systemeinstellungen neu sortiert: Host + Netzwerkeinstellungen (beide Host-Konfiguration) nebeneinander, dann Logs, dann Prüfintervall. Netzwerkeinstellungen: DNS-Anzeige zeigte bisher den lokalen systemd-resolved-Stub (127.0.0.53) aus /etc/resolv.conf statt der echten Server -- jetzt über "resolvectl dns <interface>", inkl. Fix für eine falsch geparste IPv6-Fortsetzungszeile ohne Label, und gefiltert auf IPv4 (diese Seite verwaltet nur IPv4-Konfiguration, per IPv6-Router-Advertisement gelernte Server sind hier irreführend). Dashboard: die vier Stat-Kacheln (Online/Offline/Deaktiviert/Gesamt) sind jetzt klickbare Filter (blenden alle anderen Abschnitte aus, Klick auf die aktive Kachel oder "Gesamt" hebt den Filter auf); jeder Abschnitt lässt sich unabhängig davon per Klick auf seinen Titel ein-/ ausklappen. Beide Zustände bleiben über die automatischen Live-Updates hinweg erhalten. Design-Politur: - Hauptinhaltsbereich nutzt jetzt die volle verfügbare Breite (kein Bootstrap-typisches festes max-width mehr) -- Modals bleiben bewusst bei ihrer kompakten, festen Breite Akzentfarbe exakt auf das Orange aus dem WiS-Logo abgestimmt (#ff7100, per Pixel-Sampling ermittelt) statt einer nur ähnlichen, frei gewählten Nuance - Abmelden führt zurück auf das (auch ohne Login einsehbare) Dashboard statt auf die Login-Seite Live gegen die echte Test-VM verifiziert: alle vier Logs werden geschrieben, logrotate-Konfiguration korrekt generiert, ein Zeitzonenwechsel (UTC → Europe/Berlin) wirkte sofort, DNS-Anzeige zeigt jetzt die echten IPv4-Server (1.1.1.1, 8.8.8.8) statt Stub/IPv6, Dashboard-Filter/-Collapse und Logout-Redirect per Playwright bestätigt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,21 +5,21 @@
|
||||
<div id="dashboard-tiles" data-last-run-ms="{{ last_run_epoch_ms or '' }}">
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card" data-status-filter="online">
|
||||
<div class="stat-label">Online</div>
|
||||
<div class="stat-value online">{{ stats.online }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card" data-status-filter="offline">
|
||||
<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-card" data-status-filter="disabled">
|
||||
<div class="stat-label">Deaktiviert</div>
|
||||
<div class="stat-value disabled">{{ stats.disabled }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="stat-card">
|
||||
<div class="stat-card" data-status-filter="">
|
||||
<div class="stat-label">Gesamt</div>
|
||||
<div class="stat-value">{{ stats.total }}</div>
|
||||
</div>
|
||||
@@ -54,9 +54,11 @@
|
||||
|
||||
{% if device_count %}
|
||||
|
||||
{% macro section_chevron() %}<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>{% endmacro %}
|
||||
|
||||
{% if offline_devices %}
|
||||
<div class="dash-section">
|
||||
<div class="dash-section-title">Offline <span class="pill offline">{{ offline_devices|length }}</span></div>
|
||||
<div class="dash-section" data-status="offline">
|
||||
<div class="dash-section-title">{{ section_chevron() }}Offline <span class="pill offline">{{ offline_devices|length }}</span></div>
|
||||
<div class="device-grid">
|
||||
{% for d in offline_devices %}{{ device_tile(d) }}{% endfor %}
|
||||
</div>
|
||||
@@ -64,8 +66,8 @@
|
||||
{% 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="dash-section" data-status="online">
|
||||
<div class="dash-section-title">{{ section_chevron() }}Online <span class="pill online">{{ online_devices|length }}</span></div>
|
||||
<div class="device-grid">
|
||||
{% for d in online_devices %}{{ device_tile(d) }}{% endfor %}
|
||||
</div>
|
||||
@@ -73,8 +75,8 @@
|
||||
{% 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="dash-section" data-status="disabled">
|
||||
<div class="dash-section-title">{{ section_chevron() }}Deaktiviert <span class="pill disabled">{{ disabled_devices|length }}</span></div>
|
||||
<div class="device-grid">
|
||||
{% for d in disabled_devices %}{{ device_tile(d) }}{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -90,16 +90,29 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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. Wird nach jedem
|
||||
// Live-Update erneut angewendet, damit ein aktiver Filter erhalten bleibt.
|
||||
function applySearchFilter(query) {
|
||||
const q = (query || "").trim().toLowerCase();
|
||||
// Status-Filter über die Stat-Kacheln (Online/Offline/Deaktiviert/
|
||||
// Gesamt) — null = kein Filter aktiv ("Gesamt"). Zusammen mit dem
|
||||
// Suchfilter kombiniert in applyFilters(), damit beide gleichzeitig
|
||||
// greifen können (z.B. "offline" + Namenssuche).
|
||||
let activeStatusFilter = null;
|
||||
// Welche Abschnitte (per data-status) manuell eingeklappt wurden — per
|
||||
// Klick auf den Abschnittstitel, unabhängig vom Status-Filter. Bleibt
|
||||
// über Live-Updates hinweg erhalten (siehe applyCollapseState()).
|
||||
const collapsedSections = new Set();
|
||||
|
||||
// Kombinierter Filter über alle Abschnitte hinweg: ein Abschnitt wird
|
||||
// komplett ausgeblendet, wenn er nicht zum Status-Filter passt ODER
|
||||
// keine seiner Kacheln mehr zur Namenssuche passt. Wird nach jedem
|
||||
// Live-Update erneut angewendet, damit aktive Filter erhalten bleiben
|
||||
// (die alten DOM-Knoten werden beim Austausch verworfen).
|
||||
function applyFilters() {
|
||||
const q = (searchInput ? searchInput.value : "").trim().toLowerCase();
|
||||
let anyVisible = false;
|
||||
document.querySelectorAll(".dash-section").forEach(section => {
|
||||
const statusMatches = !activeStatusFilter || section.dataset.status === activeStatusFilter;
|
||||
let sectionHasMatch = false;
|
||||
section.querySelectorAll(".device-card").forEach(card => {
|
||||
const match = card.dataset.name.toLowerCase().includes(q);
|
||||
const match = statusMatches && card.dataset.name.toLowerCase().includes(q);
|
||||
card.classList.toggle("hidden", !match);
|
||||
if (match) sectionHasMatch = true;
|
||||
});
|
||||
@@ -107,9 +120,53 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (sectionHasMatch) anyVisible = true;
|
||||
});
|
||||
const noResults = document.getElementById("noSearchResults");
|
||||
if (noResults) noResults.classList.toggle("hidden", anyVisible || q === "");
|
||||
if (noResults) noResults.classList.toggle("hidden", anyVisible || (q === "" && !activeStatusFilter));
|
||||
// Aktive Stat-Kachel synchron halten — läuft hier statt nur im
|
||||
// Klick-Handler, damit der Zustand auch nach einem Live-Update
|
||||
// (frische, neu eingefügte Stat-Kacheln) sofort wieder stimmt.
|
||||
document.querySelectorAll(".stat-card[data-status-filter]").forEach(c => {
|
||||
const isTotal = c.dataset.statusFilter === "";
|
||||
const active = activeStatusFilter === null ? isTotal : c.dataset.statusFilter === activeStatusFilter;
|
||||
c.classList.toggle("active", active);
|
||||
});
|
||||
applyCollapseState();
|
||||
}
|
||||
if (searchInput) searchInput.addEventListener("input", applyFilters);
|
||||
|
||||
// Ein-/Ausgeklappt-Zustand je Abschnitt anhand von collapsedSections
|
||||
// anwenden — separat von der Sichtbarkeit oben: "hidden" blendet den
|
||||
// ganzen Abschnitt (inkl. Titel) aus, "collapsed" versteckt nur das
|
||||
// Kachel-Raster darunter, der Titel bleibt klickbar sichtbar.
|
||||
function applyCollapseState() {
|
||||
document.querySelectorAll(".dash-section").forEach(section => {
|
||||
section.classList.toggle("collapsed", collapsedSections.has(section.dataset.status));
|
||||
});
|
||||
}
|
||||
|
||||
// Stat-Kacheln (Online/Offline/Deaktiviert/Gesamt): Klick filtert das
|
||||
// Dashboard auf genau diesen Status; erneuter Klick auf die bereits
|
||||
// aktive Kachel (oder auf "Gesamt") hebt den Filter wieder auf.
|
||||
function bindStatCards() {
|
||||
document.querySelectorAll(".stat-card[data-status-filter]").forEach(card => {
|
||||
card.addEventListener("click", function () {
|
||||
const key = this.dataset.statusFilter;
|
||||
activeStatusFilter = (!key || activeStatusFilter === key) ? null : key;
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Abschnittstitel (z.B. "Offline"): Klick klappt nur diesen Abschnitt
|
||||
// ein/aus, unabhängig vom Status-Filter oben.
|
||||
function bindSectionToggles() {
|
||||
document.querySelectorAll(".dash-section-title").forEach(title => {
|
||||
title.addEventListener("click", function () {
|
||||
const key = this.closest(".dash-section").dataset.status;
|
||||
if (collapsedSections.has(key)) collapsedSections.delete(key); else collapsedSections.add(key);
|
||||
applyCollapseState();
|
||||
});
|
||||
});
|
||||
}
|
||||
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
|
||||
@@ -156,6 +213,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
}
|
||||
bindDeviceCards();
|
||||
bindStatCards();
|
||||
bindSectionToggles();
|
||||
applyFilters();
|
||||
|
||||
// Live-Update der Dashboard-Kacheln ohne vollen Seiten-Reload: tauscht
|
||||
// nur den #dashboard-tiles-Container gegen das AJAX-Partial aus (siehe
|
||||
@@ -196,7 +256,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const pill = document.getElementById("global-timer-pill");
|
||||
if (pill) pill.dataset.lastRunMs = fresh.dataset.lastRunMs || "";
|
||||
bindDeviceCards();
|
||||
if (searchInput) applySearchFilter(searchInput.value);
|
||||
bindStatCards();
|
||||
bindSectionToggles();
|
||||
applyFilters();
|
||||
scheduleNextRefresh();
|
||||
})
|
||||
.catch(() => scheduleNextRefresh(15000));
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "logs" %}
|
||||
{% block page_title %}Kea-DHCP{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub">Log-Ausgabe des kea-dhcp4-server-Dienstes</div>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Kea-DHCP-Log</h2>
|
||||
<div class="hint">Eigene Log-Datei des DHCP-Dienstes (siehe Systemeinstellungen → Logs für Rotation/Aufbewahrung).</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>
|
||||
<span class="text-faint mono" style="font-size:11.5px;">kea-dhcp4.log</span>
|
||||
</div>
|
||||
<div id="log-box">{{ log_content or "Keine Logdatei gefunden." }}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function colorizeLine(line) {
|
||||
let cls = "";
|
||||
// Kea-Zeilenformat: "<ts> <SEVERITY> [logger/pid.tid] MESSAGE_ID text".
|
||||
// Grobe Einfärbung nach Schweregrad, ohne die Logik des Live-Logs
|
||||
// (online/offline) hier künstlich nachzubilden.
|
||||
if (/\s(FATAL|ERROR)\s/.test(line)) cls = "offline";
|
||||
else if (/\sWARN\s/.test(line)) cls = "restart";
|
||||
const span = document.createElement("span");
|
||||
span.className = "log-line" + (cls ? " " + cls : "");
|
||||
span.textContent = line;
|
||||
return span;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
function renderLog(text) {
|
||||
const box = document.getElementById("log-box");
|
||||
box.innerHTML = "";
|
||||
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;
|
||||
}
|
||||
|
||||
function fetchLog() {
|
||||
fetch("{{ url_for('get_kea_log') }}")
|
||||
.then(r => r.text().then(renderLog))
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
document.getElementById("refresh-btn").addEventListener("click", fetchLog);
|
||||
fetchLog();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -9,36 +9,8 @@
|
||||
<div class="card card-pad">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Prüfintervall</h2>
|
||||
<div class="hint">Wie oft sollen Geräte auf Erreichbarkeit geprüft werden?</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has_permission('settings_system.edit') %}
|
||||
<form method="post">
|
||||
<div class="field">
|
||||
<label for="interval">Intervall (Minuten)</label>
|
||||
<input type="number" name="interval" id="interval" value="{{ interval }}" min="1" required>
|
||||
<div class="field-hint">Der Hintergrund-Dienst (rpi-check.service) wird nach dem Speichern automatisch neu gestartet.</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<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 & Service neustarten
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="field">
|
||||
<label>Intervall (Minuten)</label>
|
||||
<input type="number" value="{{ interval }}" disabled>
|
||||
<div class="field-hint">Nur Lesezugriff — für Änderungen fehlt das Recht „Systemeinstellungen ändern“.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Hostname</h2>
|
||||
<div class="hint">Name dieses Hosts im Netzwerk.</div>
|
||||
<h2 style="font-size:16px;">Host</h2>
|
||||
<div class="hint">Name und Zeitzone dieses Hosts.</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has_permission('settings_system.edit') %}
|
||||
@@ -52,15 +24,35 @@
|
||||
Hostname setzen
|
||||
</button>
|
||||
</form>
|
||||
<hr style="border:none; border-top:1px solid var(--border-soft); margin:18px 0;">
|
||||
<form method="post">
|
||||
<div class="field">
|
||||
<label for="timezone">Zeitzone</label>
|
||||
<select name="timezone" id="timezone" required>
|
||||
{% for tz in timezones %}
|
||||
<option value="{{ tz }}" {% if tz == current_timezone %}selected{% endif %}>{{ tz }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="field-hint">Bestimmt die lokale Zeit in allen Logs und im Änderungsverlauf — wirkt sofort für diese laufende App-Instanz, ohne Dienst-Neustart.</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<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>
|
||||
Zeitzone setzen
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="field">
|
||||
<label>Hostname</label>
|
||||
<input type="text" value="{{ current_hostname or '' }}" disabled>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Zeitzone</label>
|
||||
<input type="text" value="{{ current_timezone or '' }}" disabled>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" style="grid-column:1 / -1;">
|
||||
<div class="card card-pad">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Netzwerkeinstellungen</h2>
|
||||
@@ -151,5 +143,79 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Logs</h2>
|
||||
<div class="hint">Rotation & Aufbewahrung von Live-, Änderungs-, App- und Kea-DHCP-Log.</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has_permission('settings_system.edit') %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="save_log_rotation" value="1">
|
||||
<div class="field">
|
||||
<label for="log_rotation_interval">Rotations-Intervall</label>
|
||||
<select name="log_rotation_interval" id="log_rotation_interval">
|
||||
<option value="daily" {% if log_rotation_interval == "daily" %}selected{% endif %}>Täglich</option>
|
||||
<option value="weekly" {% if log_rotation_interval == "weekly" %}selected{% endif %}>Wöchentlich</option>
|
||||
<option value="monthly" {% if log_rotation_interval == "monthly" %}selected{% endif %}>Monatlich</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="log_rotation_keep">Aufbewahrung (Anzahl Rotationen)</label>
|
||||
<input type="number" name="log_rotation_keep" id="log_rotation_keep" value="{{ log_rotation_keep }}" min="1" required>
|
||||
<div class="field-hint">Standard: wöchentlich, 4 Rotationen (≈ 1 Monat Historie je Log).</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<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>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="field">
|
||||
<label>Rotations-Intervall</label>
|
||||
<input type="text" value="{{ log_rotation_interval }}" disabled>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Aufbewahrung (Anzahl Rotationen)</label>
|
||||
<input type="text" value="{{ log_rotation_keep }}" disabled>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="field-hint mono" style="margin-top:10px; font-size:11px; line-height:1.6;">
|
||||
Live: {{ log_paths.live }}<br>
|
||||
Änderungen: {{ log_paths.changes }}<br>
|
||||
App: {{ log_paths.app }}<br>
|
||||
Kea-DHCP: {{ log_paths.kea }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Prüfintervall</h2>
|
||||
<div class="hint">Wie oft sollen Geräte auf Erreichbarkeit geprüft werden?</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has_permission('settings_system.edit') %}
|
||||
<form method="post">
|
||||
<div class="field">
|
||||
<label for="interval">Intervall (Minuten)</label>
|
||||
<input type="number" name="interval" id="interval" value="{{ interval }}" min="1" required>
|
||||
<div class="field-hint">Der Hintergrund-Dienst (rpi-check.service) wird nach dem Speichern automatisch neu gestartet.</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<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 & Service neustarten
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="field">
|
||||
<label>Intervall (Minuten)</label>
|
||||
<input type="number" value="{{ interval }}" disabled>
|
||||
<div class="field-hint">Nur Lesezugriff — für Änderungen fehlt das Recht „Systemeinstellungen ändern“.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user