diff --git a/src/lib/db.ts b/src/lib/db.ts index 4914b3c..fc99347 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -17,6 +17,20 @@ import type { Protocol } from './types'; const DB_NAME = 'netdiag'; const LS_PREFIX = 'netdiag.protocol.'; +/** + * Fehlende optionale Felder eines geladenen Protokolls defensiv ergänzen. + * Ältere Protokolle (vor den Geräte-Features) haben weder `savedScans` noch + * `monitorSessions` — ohne diese Normalisierung liefen Komponenten auf + * `undefined.map`. Mutiert das Objekt und gibt es zurück. + */ +function normalizeProtocol(p: Protocol): Protocol { + p.devices ??= []; + p.measurements ??= []; + p.savedScans ??= []; + p.monitorSessions ??= []; + return p; +} + let useSqlite = false; let db: SQLiteDBConnection | null = null; @@ -66,10 +80,10 @@ export async function getProtocol(uuid: string): Promise { if (useSqlite && db) { const res = await db.query('SELECT json FROM protocols WHERE uuid = ?', [uuid]); const row = res.values?.[0]; - return row ? (JSON.parse(row.json) as Protocol) : null; + return row ? normalizeProtocol(JSON.parse(row.json) as Protocol) : null; } const raw = localStorage.getItem(LS_PREFIX + uuid); - return raw ? (JSON.parse(raw) as Protocol) : null; + return raw ? normalizeProtocol(JSON.parse(raw) as Protocol) : null; } /** Alle Protokolle laden (neueste zuerst) */ @@ -77,12 +91,12 @@ export async function getAllProtocols(): Promise { let list: Protocol[] = []; if (useSqlite && db) { const res = await db.query('SELECT json FROM protocols ORDER BY updated_at DESC'); - list = (res.values ?? []).map((r) => JSON.parse(r.json) as Protocol); + list = (res.values ?? []).map((r) => normalizeProtocol(JSON.parse(r.json) as Protocol)); } else { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(LS_PREFIX)) { - list.push(JSON.parse(localStorage.getItem(key)!) as Protocol); + list.push(normalizeProtocol(JSON.parse(localStorage.getItem(key)!) as Protocol)); } } list.sort((a, b) => b.updatedAt - a.updatedAt); diff --git a/src/lib/types.ts b/src/lib/types.ts index bdef981..e51cdd7 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -54,6 +54,54 @@ export interface Device { vendor?: string; deviceType?: string; note?: string; + /** vom Benutzer als Favorit markiert (bleibt auch ohne gespeicherten Scan erhalten) */ + isFavorite?: boolean; + /** benutzervergebener Name, überschreibt hostname in der Anzeige */ + customName?: string; + /** Unix-Zeit der letzten Sichtung im Netz */ + lastSeen?: number; + /** zuletzt beim Port-Scan gefundene offene Ports */ + openPorts?: number[]; + /** NetBIOS-Name (UDP-137-Abfrage) */ + netbiosName?: string; + /** mDNS/Bonjour-Name */ + mdnsName?: string; + /** angebotene mDNS-Dienste, z.B. ['_printer._tcp', '_googlecast._tcp'] */ + mdnsServices?: string[]; +} + +/** Eingefrorener Snapshot eines IP-Scans (wieder aufrufbar) */ +export interface SavedScan { + id: string; + name: string; + /** Unix-Zeit der Speicherung */ + createdAt: number; + subnet: string; + devices: Device[]; +} + +/** Ein Ereignis der Geräte-Überwachung */ +export interface MonitorEvent { + /** Unix-Zeit des Ereignisses */ + ts: number; + ip: string; + type: 'down' | 'up'; + /** Dauer des vorangegangenen Ausfalls in Sekunden (nur bei 'up') */ + durationSec?: number; +} + +/** Überwachungs-Sitzung des Geräte-Monitors (Kamera-Problem) */ +export interface DeviceMonitorSession { + id: string; + name: string; + startedAt: number; + endedAt?: number; + /** Prüfintervall in Sekunden */ + intervalSec: number; + /** überwachte Geräte */ + targets: { ip: string; label: string }[]; + events: MonitorEvent[]; + status: 'running' | 'stopped'; } /** Ampel-Bewertung einer Messung */ @@ -92,6 +140,10 @@ export interface Protocol { note: string; devices: Device[]; measurements: Measurement[]; + /** gespeicherte IP-Scan-Snapshots (nur lokal, wird nicht synchronisiert) */ + savedScans?: SavedScan[]; + /** Geräte-Überwachungs-Sitzungen (nur lokal, wird nicht synchronisiert) */ + monitorSessions?: DeviceMonitorSession[]; /** true solange noch nicht zum Server synchronisiert */ dirty: boolean; updatedAt: number; diff --git a/src/routes/auftraege/+page.svelte b/src/routes/auftraege/+page.svelte index f3d957c..5979ca6 100644 --- a/src/routes/auftraege/+page.svelte +++ b/src/routes/auftraege/+page.svelte @@ -12,12 +12,15 @@ let orders = $state([]); let search = $state(''); - // Standard: alle Aufträge, zuletzt bearbeitete zuerst (Server sortiert nach tms). - // Haken setzen blendet abgeschlossene aus und zeigt nur aktive Aufträge. - let onlyActive = $state(false); + // Standard: nur aktive Aufträge. Haken entfernen zeigt auch abgeschlossene. + // Sortierung: Aufträge mit lokaler Scan-Tätigkeit zuerst, dann Server-Reihenfolge (tms). + let onlyActive = $state(true); let loading = $state(false); let loadError = $state(''); + /** orderId → letzte lokale Protokoll-Bearbeitung + Anzahl (für Sortierung/Anzeige) */ + let localActivity = $state(new Map()); + let searchTimer: ReturnType; /** Kunde + Ort als eine Zeile — das, was man im Kopf hat */ @@ -43,7 +46,23 @@ loadError = ''; try { const res = await listOrders({ open: onlyActive, q: search.trim() || undefined }); - orders = res.orders; + // Lokale Protokoll-Tätigkeit erfassen — ein Scan auf dem Handy ändert die + // Dolibarr-tms NICHT, daher hier clientseitig nach oben sortieren. + const activity = new Map(); + for (const p of await getAllProtocols()) { + if (p.orderId == null) continue; + const cur = activity.get(p.orderId); + activity.set(p.orderId, { + updatedAt: Math.max(cur?.updatedAt ?? 0, p.updatedAt), + count: (cur?.count ?? 0) + 1, + }); + } + localActivity = activity; + // Aufträge mit lokaler Tätigkeit zuerst (jüngste oben), Rest in + // Server-Reihenfolge (tms DESC) — Array.sort ist stabil. + orders = res.orders + .slice() + .sort((a, b) => (activity.get(b.id)?.updatedAt ?? 0) - (activity.get(a.id)?.updatedAt ?? 0)); } catch (e) { loadError = e instanceof ApiError ? e.message : 'Laden fehlgeschlagen'; } finally { @@ -51,6 +70,15 @@ } } + /** Datums-Zusatz für die Auftragszeile: lokale Bearbeitung bevorzugt */ + function editedInfo(order: Order): string { + const local = localActivity.get(order.id); + if (local) return ' · zuletzt bearb. ' + fmtDate(local.updatedAt); + if (order.tms) return ' · bearb. ' + fmtDate(order.tms); + if (order.date) return ' · ' + fmtDate(order.date); + return ''; + } + function onSearchInput() { clearTimeout(searchTimer); searchTimer = setTimeout(load, 300); @@ -84,7 +112,8 @@ } onMount(async () => { - onlyActive = (await Preferences.get({ key: 'nd_only_active' })).value === '1'; + const pref = (await Preferences.get({ key: 'nd_only_active' })).value; + onlyActive = pref == null ? true : pref === '1'; load(); }); @@ -121,6 +150,7 @@

Keine Aufträge gefunden.

{:else} {#each orders as order (order.id)} + {@const pc = Math.max(order.protocolCount ?? 0, localActivity.get(order.id)?.count ?? 0)}