diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2e317b0..6378887 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -33,6 +33,12 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"> + + + @@ -45,5 +51,8 @@ + + + diff --git a/android/app/src/main/java/de/data_it_solution/netdiag/MonitorService.kt b/android/app/src/main/java/de/data_it_solution/netdiag/MonitorService.kt new file mode 100644 index 0000000..9bd98bf --- /dev/null +++ b/android/app/src/main/java/de/data_it_solution/netdiag/MonitorService.kt @@ -0,0 +1,81 @@ +package de.data_it_solution.netdiag + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat + +/** + * Schlanker Vordergrund-Dienst für den Geräte-Monitor. + * + * Er hält den App-Prozess am Leben, solange die Überwachung läuft — damit + * Android die Mess-Schleife bei ausgeschaltetem Display oder App-Wechsel nicht + * beendet. Die eigentliche Ping-Logik läuft im NetDiagScannerPlugin; dieser + * Dienst zeigt nur die dauerhafte Benachrichtigung. + */ +class MonitorService : Service() { + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val text = intent?.getStringExtra(EXTRA_TEXT) ?: "Geräte-Überwachung läuft" + val notification = buildNotification(text) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NOTIF_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else { + startForeground(NOTIF_ID, notification) + } + return START_STICKY + } + + private fun buildNotification(text: String): Notification { + ensureChannel(this) + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("NetDiag — Geräte-Monitor") + .setContentText(text) + .setSmallIcon(android.R.drawable.ic_menu_compass) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + } + + companion object { + const val CHANNEL_ID = "netdiag-monitor" + const val NOTIF_ID = 4711 + const val EXTRA_TEXT = "text" + + /** Benachrichtigungskanal anlegen (idempotent) */ + fun ensureChannel(ctx: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + val mgr = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (mgr.getNotificationChannel(CHANNEL_ID) == null) { + mgr.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + "Geräte-Monitor", + NotificationManager.IMPORTANCE_LOW, + ), + ) + } + } + + fun start(ctx: Context, text: String) { + val i = Intent(ctx, MonitorService::class.java).putExtra(EXTRA_TEXT, text) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(i) + } else { + ctx.startService(i) + } + } + + fun stop(ctx: Context) { + ctx.stopService(Intent(ctx, MonitorService::class.java)) + } + } +} diff --git a/android/app/src/main/java/de/data_it_solution/netdiag/NetDiagScannerPlugin.kt b/android/app/src/main/java/de/data_it_solution/netdiag/NetDiagScannerPlugin.kt index 1102e1d..6fb14dd 100644 --- a/android/app/src/main/java/de/data_it_solution/netdiag/NetDiagScannerPlugin.kt +++ b/android/app/src/main/java/de/data_it_solution/netdiag/NetDiagScannerPlugin.kt @@ -1,6 +1,7 @@ package de.data_it_solution.netdiag import android.Manifest +import android.app.NotificationManager import android.content.Context import android.content.Intent import android.net.ConnectivityManager @@ -10,6 +11,7 @@ import android.net.nsd.NsdServiceInfo import android.net.wifi.WifiManager import android.os.Build import android.provider.Settings +import androidx.core.app.NotificationCompat import androidx.core.content.FileProvider import com.getcapacitor.JSArray import com.getcapacitor.JSObject @@ -825,6 +827,140 @@ class NetDiagScannerPlugin : Plugin() { var maxMs = 0.0 } + /* --------------------------------------------------------------------- */ + /* Geräte-Monitor — Dauerüberwachung mehrerer Geräte (Kamera-Problem) */ + /* --------------------------------------------------------------------- */ + + private val monitorRuns = ConcurrentHashMap() + + private class MonitorRun( + val targets: List>, + val intervalSec: Int, + ) { + @Volatile var active = true + /** je IP: true = erreichbar */ + val state = ConcurrentHashMap() + /** je IP: Beginn des aktuellen Ausfalls (für die Ausfalldauer) */ + val downSince = ConcurrentHashMap() + /** alle bisher erzeugten Ereignisse — für die UI-Wiederaufnahme */ + val events = java.util.concurrent.CopyOnWriteArrayList() + } + + /** + * Überwacht die Erreichbarkeit mehrerer Geräte im festen Intervall. + * Jeder Wechsel erreichbar↔nicht erreichbar erzeugt ein `monitorEvent`. + * Ein Vordergrund-Dienst hält den Prozess am Leben (Display aus / App-Wechsel), + * ein WifiLock verhindert, dass das WLAN zwischen den Messungen schläft. + */ + @PluginMethod + fun startMonitor(call: PluginCall) { + val arr = call.getArray("hosts") ?: return call.reject("hosts fehlt") + val intervalSec = (call.getInt("intervalSec") ?: 30).coerceIn(5, 600) + val targets = ArrayList>() + for (i in 0 until arr.length()) { + val o = arr.optJSONObject(i) ?: continue + val ip = o.optString("ip") + if (ip.isNotEmpty()) targets.add(ip to o.optString("label", ip)) + } + if (targets.isEmpty()) return call.reject("keine Geräte gewählt") + + val runId = "mon-${System.currentTimeMillis()}" + val run = MonitorRun(targets, intervalSec) + monitorRuns[runId] = run + MonitorService.start(context, "${targets.size} Geräte · alle ${intervalSec}s") + + @Suppress("DEPRECATION") + val wifiLock = (context.applicationContext + .getSystemService(Context.WIFI_SERVICE) as WifiManager) + .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "netdiag-monitor") + try { wifiLock.acquire() } catch (_: Exception) { } + + io.launch { + try { + // Ausgangslage erfassen — erzeugt noch kein Ereignis + for ((ip, _) in targets) run.state[ip] = isReachable(ip) + while (run.active) { + Thread.sleep(intervalSec * 1000L) + if (!run.active) break + for ((ip, label) in targets) { + if (!run.active) break + val up = isReachable(ip) + val prev = run.state[ip] ?: up + if (up == prev) continue + run.state[ip] = up + val now = System.currentTimeMillis() + val ev = JSObject() + .put("runId", runId) + .put("ip", ip) + .put("label", label) + .put("ts", now) + if (up) { + val since = run.downSince.remove(ip) + ev.put("type", "up") + ev.put( + "durationSec", + if (since != null) ((now - since) / 1000L).toInt() else 0, + ) + } else { + run.downSince[ip] = now + ev.put("type", "down") + notifyDown(label, ip) + } + run.events.add(ev) + notifyListeners("monitorEvent", ev) + } + } + } finally { + try { if (wifiLock.isHeld) wifiLock.release() } catch (_: Exception) { } + } + } + resolve(call, JSObject().put("runId", runId)) + } + + @PluginMethod + fun stopMonitor(call: PluginCall) { + val runId = call.getString("runId") ?: return call.reject("runId fehlt") + val run = monitorRuns.remove(runId) ?: return call.reject("Lauf nicht gefunden") + run.active = false + if (monitorRuns.isEmpty()) MonitorService.stop(context) + val events = JSArray() + run.events.forEach { events.put(it) } + resolve(call, JSObject().put("stopped", true).put("events", events)) + } + + /** Status eines Monitor-Laufs abfragen (UI-Wiederaufnahme nach Seitenwechsel) */ + @PluginMethod + fun getMonitorStatus(call: PluginCall) { + val runId = call.getString("runId") ?: return call.reject("runId fehlt") + val run = monitorRuns[runId] + val events = JSArray() + run?.events?.forEach { events.put(it) } + resolve(call, JSObject() + .put("running", run != null && run.active) + .put("events", events)) + } + + private fun isReachable(ip: String): Boolean = try { + InetAddress.getByName(ip).isReachable(1500) + } catch (_: Exception) { + false + } + + /** Heads-up-Benachrichtigung, wenn ein überwachtes Gerät ausfällt */ + private fun notifyDown(label: String, ip: String) { + try { + MonitorService.ensureChannel(context) + val mgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val n = NotificationCompat.Builder(context, MonitorService.CHANNEL_ID) + .setContentTitle("Gerät nicht erreichbar") + .setContentText("$label ($ip) antwortet nicht mehr") + .setSmallIcon(android.R.drawable.stat_notify_error) + .setAutoCancel(true) + .build() + mgr.notify(ip.hashCode(), n) + } catch (_: Exception) { } + } + /* --------------------------------------------------------------------- */ /* App-Update: APK herunterladen und Paketinstaller öffnen */ /* --------------------------------------------------------------------- */ diff --git a/native-plugin/MonitorService.kt b/native-plugin/MonitorService.kt new file mode 100644 index 0000000..9bd98bf --- /dev/null +++ b/native-plugin/MonitorService.kt @@ -0,0 +1,81 @@ +package de.data_it_solution.netdiag + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat + +/** + * Schlanker Vordergrund-Dienst für den Geräte-Monitor. + * + * Er hält den App-Prozess am Leben, solange die Überwachung läuft — damit + * Android die Mess-Schleife bei ausgeschaltetem Display oder App-Wechsel nicht + * beendet. Die eigentliche Ping-Logik läuft im NetDiagScannerPlugin; dieser + * Dienst zeigt nur die dauerhafte Benachrichtigung. + */ +class MonitorService : Service() { + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val text = intent?.getStringExtra(EXTRA_TEXT) ?: "Geräte-Überwachung läuft" + val notification = buildNotification(text) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NOTIF_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else { + startForeground(NOTIF_ID, notification) + } + return START_STICKY + } + + private fun buildNotification(text: String): Notification { + ensureChannel(this) + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("NetDiag — Geräte-Monitor") + .setContentText(text) + .setSmallIcon(android.R.drawable.ic_menu_compass) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + } + + companion object { + const val CHANNEL_ID = "netdiag-monitor" + const val NOTIF_ID = 4711 + const val EXTRA_TEXT = "text" + + /** Benachrichtigungskanal anlegen (idempotent) */ + fun ensureChannel(ctx: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + val mgr = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (mgr.getNotificationChannel(CHANNEL_ID) == null) { + mgr.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + "Geräte-Monitor", + NotificationManager.IMPORTANCE_LOW, + ), + ) + } + } + + fun start(ctx: Context, text: String) { + val i = Intent(ctx, MonitorService::class.java).putExtra(EXTRA_TEXT, text) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(i) + } else { + ctx.startService(i) + } + } + + fun stop(ctx: Context) { + ctx.stopService(Intent(ctx, MonitorService::class.java)) + } + } +} diff --git a/native-plugin/NetDiagScannerPlugin.kt b/native-plugin/NetDiagScannerPlugin.kt index 1102e1d..6fb14dd 100644 --- a/native-plugin/NetDiagScannerPlugin.kt +++ b/native-plugin/NetDiagScannerPlugin.kt @@ -1,6 +1,7 @@ package de.data_it_solution.netdiag import android.Manifest +import android.app.NotificationManager import android.content.Context import android.content.Intent import android.net.ConnectivityManager @@ -10,6 +11,7 @@ import android.net.nsd.NsdServiceInfo import android.net.wifi.WifiManager import android.os.Build import android.provider.Settings +import androidx.core.app.NotificationCompat import androidx.core.content.FileProvider import com.getcapacitor.JSArray import com.getcapacitor.JSObject @@ -825,6 +827,140 @@ class NetDiagScannerPlugin : Plugin() { var maxMs = 0.0 } + /* --------------------------------------------------------------------- */ + /* Geräte-Monitor — Dauerüberwachung mehrerer Geräte (Kamera-Problem) */ + /* --------------------------------------------------------------------- */ + + private val monitorRuns = ConcurrentHashMap() + + private class MonitorRun( + val targets: List>, + val intervalSec: Int, + ) { + @Volatile var active = true + /** je IP: true = erreichbar */ + val state = ConcurrentHashMap() + /** je IP: Beginn des aktuellen Ausfalls (für die Ausfalldauer) */ + val downSince = ConcurrentHashMap() + /** alle bisher erzeugten Ereignisse — für die UI-Wiederaufnahme */ + val events = java.util.concurrent.CopyOnWriteArrayList() + } + + /** + * Überwacht die Erreichbarkeit mehrerer Geräte im festen Intervall. + * Jeder Wechsel erreichbar↔nicht erreichbar erzeugt ein `monitorEvent`. + * Ein Vordergrund-Dienst hält den Prozess am Leben (Display aus / App-Wechsel), + * ein WifiLock verhindert, dass das WLAN zwischen den Messungen schläft. + */ + @PluginMethod + fun startMonitor(call: PluginCall) { + val arr = call.getArray("hosts") ?: return call.reject("hosts fehlt") + val intervalSec = (call.getInt("intervalSec") ?: 30).coerceIn(5, 600) + val targets = ArrayList>() + for (i in 0 until arr.length()) { + val o = arr.optJSONObject(i) ?: continue + val ip = o.optString("ip") + if (ip.isNotEmpty()) targets.add(ip to o.optString("label", ip)) + } + if (targets.isEmpty()) return call.reject("keine Geräte gewählt") + + val runId = "mon-${System.currentTimeMillis()}" + val run = MonitorRun(targets, intervalSec) + monitorRuns[runId] = run + MonitorService.start(context, "${targets.size} Geräte · alle ${intervalSec}s") + + @Suppress("DEPRECATION") + val wifiLock = (context.applicationContext + .getSystemService(Context.WIFI_SERVICE) as WifiManager) + .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "netdiag-monitor") + try { wifiLock.acquire() } catch (_: Exception) { } + + io.launch { + try { + // Ausgangslage erfassen — erzeugt noch kein Ereignis + for ((ip, _) in targets) run.state[ip] = isReachable(ip) + while (run.active) { + Thread.sleep(intervalSec * 1000L) + if (!run.active) break + for ((ip, label) in targets) { + if (!run.active) break + val up = isReachable(ip) + val prev = run.state[ip] ?: up + if (up == prev) continue + run.state[ip] = up + val now = System.currentTimeMillis() + val ev = JSObject() + .put("runId", runId) + .put("ip", ip) + .put("label", label) + .put("ts", now) + if (up) { + val since = run.downSince.remove(ip) + ev.put("type", "up") + ev.put( + "durationSec", + if (since != null) ((now - since) / 1000L).toInt() else 0, + ) + } else { + run.downSince[ip] = now + ev.put("type", "down") + notifyDown(label, ip) + } + run.events.add(ev) + notifyListeners("monitorEvent", ev) + } + } + } finally { + try { if (wifiLock.isHeld) wifiLock.release() } catch (_: Exception) { } + } + } + resolve(call, JSObject().put("runId", runId)) + } + + @PluginMethod + fun stopMonitor(call: PluginCall) { + val runId = call.getString("runId") ?: return call.reject("runId fehlt") + val run = monitorRuns.remove(runId) ?: return call.reject("Lauf nicht gefunden") + run.active = false + if (monitorRuns.isEmpty()) MonitorService.stop(context) + val events = JSArray() + run.events.forEach { events.put(it) } + resolve(call, JSObject().put("stopped", true).put("events", events)) + } + + /** Status eines Monitor-Laufs abfragen (UI-Wiederaufnahme nach Seitenwechsel) */ + @PluginMethod + fun getMonitorStatus(call: PluginCall) { + val runId = call.getString("runId") ?: return call.reject("runId fehlt") + val run = monitorRuns[runId] + val events = JSArray() + run?.events?.forEach { events.put(it) } + resolve(call, JSObject() + .put("running", run != null && run.active) + .put("events", events)) + } + + private fun isReachable(ip: String): Boolean = try { + InetAddress.getByName(ip).isReachable(1500) + } catch (_: Exception) { + false + } + + /** Heads-up-Benachrichtigung, wenn ein überwachtes Gerät ausfällt */ + private fun notifyDown(label: String, ip: String) { + try { + MonitorService.ensureChannel(context) + val mgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val n = NotificationCompat.Builder(context, MonitorService.CHANNEL_ID) + .setContentTitle("Gerät nicht erreichbar") + .setContentText("$label ($ip) antwortet nicht mehr") + .setSmallIcon(android.R.drawable.stat_notify_error) + .setAutoCancel(true) + .build() + mgr.notify(ip.hashCode(), n) + } catch (_: Exception) { } + } + /* --------------------------------------------------------------------- */ /* App-Update: APK herunterladen und Paketinstaller öffnen */ /* --------------------------------------------------------------------- */ diff --git a/src/lib/components/DeviceMultiSelect.svelte b/src/lib/components/DeviceMultiSelect.svelte new file mode 100644 index 0000000..db8d43e --- /dev/null +++ b/src/lib/components/DeviceMultiSelect.svelte @@ -0,0 +1,36 @@ + + +
+ {#each devices as d (d.clientId)} + + {/each} +
diff --git a/src/lib/scanner.ts b/src/lib/scanner.ts index d03b9d4..9b3d87f 100644 --- a/src/lib/scanner.ts +++ b/src/lib/scanner.ts @@ -6,7 +6,7 @@ * Beispieldaten, damit die Oberfläche ohne Gerät entwickelt werden kann. */ -import { Capacitor, registerPlugin } from '@capacitor/core'; +import { Capacitor, registerPlugin, type PluginListenerHandle } from '@capacitor/core'; /* --- Datentypen der Plugin-Antworten --- */ @@ -40,6 +40,16 @@ export interface ConflictScanResult { /** false = ARP-Tabelle nicht lesbar (Android-Einschränkung, dann keine Aussage möglich) */ arpAvailable: boolean; } +/** Ein Ereignis des Geräte-Monitors (vom nativen Plugin geliefert) */ +export interface MonitorEventData { + runId: string; + ip: string; + label: string; + ts: number; + type: 'up' | 'down'; + /** Dauer des vorangegangenen Ausfalls in Sekunden (nur bei 'up') */ + durationSec?: number; +} export interface OpenPort { port: number; service?: string; @@ -119,6 +129,17 @@ export interface NetDiagScannerPlugin { avgMs: number; maxMs: number; }>; + /** Geräte-Monitor starten: mehrere Geräte im Intervall auf Erreichbarkeit prüfen */ + startMonitor(opts: { + hosts: { ip: string; label: string }[]; + intervalSec: number; + }): Promise<{ runId: string }>; + /** Geräte-Monitor beenden — liefert alle gesammelten Ereignisse */ + stopMonitor(opts: { runId: string }): Promise<{ stopped: boolean; events: MonitorEventData[] }>; + /** Status eines Monitor-Laufs abfragen (Wiederaufnahme nach Seitenwechsel) */ + getMonitorStatus(opts: { + runId: string; + }): Promise<{ running: boolean; events: MonitorEventData[] }>; } const native = registerPlugin('NetDiagScanner'); @@ -131,6 +152,11 @@ function rnd(min: number, max: number): number { return Math.round((min + Math.random() * (max - min)) * 10) / 10; } +/* --- Geräte-Monitor: Ereignis-Verteilung + Mock-Simulation --- */ +const monitorListeners = new Set<(e: MonitorEventData) => void>(); +let mockMonitorTimer: ReturnType | undefined; +let mockMonitorEvents: MonitorEventData[] = []; + const mock: NetDiagScannerPlugin = { async getLocalSubnet() { return { subnet: '192.168.1.0/24', ip: '192.168.1.50', gateway: '192.168.1.1' }; @@ -260,7 +286,59 @@ const mock: NetDiagScannerPlugin = { async stopStressTest() { return { samples: 120, lossPct: rnd(0, 2), avgMs: rnd(3, 10), maxMs: rnd(20, 90) }; }, + async startMonitor(opts) { + const runId = 'mock-mon-' + Date.now(); + mockMonitorEvents = []; + mockMonitorTimer = setInterval(() => { + const host = opts.hosts[Math.floor(Math.random() * opts.hosts.length)]; + if (!host) return; + const type: 'up' | 'down' = Math.random() < 0.5 ? 'down' : 'up'; + const ev: MonitorEventData = { + runId, + ip: host.ip, + label: host.label, + ts: Date.now(), + type, + durationSec: type === 'up' ? Math.floor(rnd(20, 180)) : undefined, + }; + mockMonitorEvents.push(ev); + monitorListeners.forEach((cb) => cb(ev)); + }, 6000); + return { runId }; + }, + async stopMonitor() { + if (mockMonitorTimer) clearInterval(mockMonitorTimer); + mockMonitorTimer = undefined; + return { stopped: true, events: mockMonitorEvents }; + }, + async getMonitorStatus() { + return { running: mockMonitorTimer !== undefined, events: mockMonitorEvents }; + }, }; /** Aktive Scanner-Implementierung: nativ auf dem Gerät, Mock im Browser */ export const scanner: NetDiagScannerPlugin = Capacitor.isNativePlatform() ? native : mock; + +/** + * Auf Geräte-Monitor-Ereignisse hören. Gibt die Abmeldefunktion zurück. + * Nativ: Listener am Plugin; im Browser-Dev: simulierte Mock-Ereignisse. + */ +export function onMonitorEvent(cb: (e: MonitorEventData) => void): () => void { + if (Capacitor.isNativePlatform()) { + const handle = ( + native as unknown as { + addListener( + name: string, + cb: (e: MonitorEventData) => void, + ): Promise; + } + ).addListener('monitorEvent', cb); + return () => { + void handle.then((h) => h.remove()); + }; + } + monitorListeners.add(cb); + return () => { + monitorListeners.delete(cb); + }; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index e51cdd7..1c9b408 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -102,6 +102,8 @@ export interface DeviceMonitorSession { targets: { ip: string; label: string }[]; events: MonitorEvent[]; status: 'running' | 'stopped'; + /** laufende Plugin-Lauf-ID (für Wiederaufnahme nach Seitenwechsel) */ + runId?: string; } /** Ampel-Bewertung einer Messung */ diff --git a/src/routes/protokoll/[id]/+page.svelte b/src/routes/protokoll/[id]/+page.svelte index 2c7433f..1094234 100644 --- a/src/routes/protokoll/[id]/+page.svelte +++ b/src/routes/protokoll/[id]/+page.svelte @@ -267,6 +267,17 @@ {tool.description} {/each} + + + + Geräte-Monitor + + Erreichbarkeit mehrerer Geräte dauerhaft überwachen. + + diff --git a/src/routes/protokoll/[id]/monitor/+page.svelte b/src/routes/protokoll/[id]/monitor/+page.svelte new file mode 100644 index 0000000..08b5888 --- /dev/null +++ b/src/routes/protokoll/[id]/monitor/+page.svelte @@ -0,0 +1,298 @@ + + +{#snippet eventRow(s: DeviceMonitorSession, e: MonitorEvent)} +
+ + {fmtTime(e.ts)} + {labelFor(s, e.ip)} + + {e.type === 'down' + ? 'Ausfall' + : 'wieder da' + (e.durationSec ? ' (' + fmtDuration(e.durationSec) + ')' : '')} + +
+{/snippet} + +{#if protocol} + + +
+ {#if running && session} + +
+
+ + Überwachung läuft +
+

+ {session.targets.length} Geräte · alle {session.intervalSec}s · seit + {fmtTime(session.startedAt)} +

+ +
+ +

+ Ereignisse ({session.events.length}) +

+ {#if session.events.length === 0} +

Noch kein Ausfall — alle Geräte erreichbar.

+ {/if} + {#each [...session.events].reverse() as e (e.ts + '-' + e.ip)} + {@render eventRow(session, e)} + {/each} + {:else} + + {#if protocol.devices.length === 0} +

+ Noch keine Geräte — bitte zuerst einen IP-Scan ausführen. +

+ {:else} +

Geräte zum Überwachen wählen

+ + + + + +

+ Läuft auch bei ausgeschaltetem Display weiter und meldet jeden Ausfall mit + Uhrzeit — gut, um sporadisch ausfallende Geräte einzukreisen. +

+ {/if} + {/if} + + + {#if pastSessions.length > 0} +

Frühere Überwachungen

+ {#each pastSessions as s (s.id)} +
+ + {#if expanded === s.id} +
+ {#if s.events.length === 0} +

Kein Ausfall während der Überwachung.

+ {/if} + {#each [...s.events].reverse() as e (e.ts + '-' + e.ip)} + {@render eventRow(s, e)} + {/each} +
+ {/if} +
+ {/each} + {/if} +
+{:else} +
Lädt …
+{/if}