From 69a4c507111f4eec2fec2e3719c2d220883af162 Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Sun, 16 Aug 2026 11:21:59 +0200 Subject: [PATCH] =?UTF-8?q?Phase=206:=20Lesbarkeit=20und=20Bedienung=20?= =?UTF-8?q?=E2=80=94=20Ampel=20mit=20Form,=20Klartext-Werte,=20Toasts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ein Befund davon ist ein Fehler aus Phase 4/5, den erst die Bestandsprüfung zutage gebracht hat: - MeasurementResult konnte nur Skalare und Textlisten. Die WLAN-Kanalmessung besteht aber aus verschachtelten Objekten (eigenesNetz, netze, empfehlung24GHz) — auf dem Handy stand dort "[object Object]", während dieselbe Messung in Dolibarr als saubere Tabelle aussah. Jetzt: "Eigenes Netz: FRITZ!Box 7590 SM · Kanal 6 · 2.4 GHz · -50 dBm · sehr gut". Weiter: - Ampel mit Form UND Zeichen statt nur Farbe (AmpelBadge.svelte): Kreis ✓, Dreieck !, Quadrat ✕, Raute ?. Rot-Grün-Sehschwäche betrifft rund 9 % der Männer, und im Sonnenlicht sind gesättigte Farben auf dem Handy ohnehin kaum zu unterscheiden. Ersetzt drei duplizierte Farbpunkt-Stellen. - Zeitstempel je Messung — bei mehreren Läufen desselben Werkzeugs war sonst nicht erkennbar, welche von wann ist. - messfelder.ts: deutsche Bezeichnungen mit Einheiten, Gegenstück zu netdiagKundenfelder() im Modul. Aus "verlustProzent: 0 · minMs: 4.4" wird "Paketverlust: 0 % · Kürzeste Antwortzeit: 4.4 ms". Bewusst KEINE Whitelist wie im Kundendokument — die App ist die Technikeransicht, unbekannte Schlüssel werden lesbar gemacht statt ausgeblendet. - Werkzeug-Klarnamen auch in der App: Messungen aus eigenen Routen (WLAN-Kanalanalyse, Monitor, IP-Test) haben keine Registry-ID, dort stand die rohe ID "wifikanal". - Fehler-Toasts bleiben 12 s statt 3 s stehen, erscheinen unten im Daumenbereich statt oben am Notch und lassen sich zum Schließen antippen. Sie weichen einer festen Aktionsleiste aus, statt den "Abschließen"-Knopf zu verdecken. - Kontrast: Messwerte von text-zinc-500 (3,67:1, unter WCAG-Minimum) auf text-zinc-400 (6,91:1), 11 -> 12 px, Zahlen mit tabular-nums. - setKeepAwake (nativ, FLAG_KEEP_SCREEN_ON): Bildschirm bleibt an, solange eine Messung läuft. Der WakeLock aus Phase 1 hält nur die CPU wach — das Display ging trotzdem aus, und man musste beim Zusehen ständig antippen. Im Emulator geprüft: WLAN-Kanalmessung wird vollständig lesbar dargestellt (Kennzahlen, Kanalempfehlung, Hinweise, Netzliste), Ampel als Kreis mit Haken, Zeitstempel, Klarname. --- .../netdiag/NetDiagScannerPlugin.kt | 30 +++ src/lib/components/AmpelBadge.svelte | 64 ++++++ src/lib/components/DeviceCard.svelte | 3 +- src/lib/components/MeasurementResult.svelte | 51 +++-- src/lib/components/Toast.svelte | 44 ++++- src/lib/components/ToolDialog.svelte | 5 + src/lib/messfelder.ts | 185 ++++++++++++++++++ src/lib/scanner.ts | 9 + src/lib/toast.svelte.ts | 18 +- src/routes/protokoll/[id]/+page.svelte | 28 ++- 10 files changed, 416 insertions(+), 21 deletions(-) create mode 100644 src/lib/components/AmpelBadge.svelte create mode 100644 src/lib/messfelder.ts 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 441849b..1ff8f62 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 @@ -1007,6 +1007,36 @@ class NetDiagScannerPlugin : Plugin() { } } + /** + * Bildschirm anlassen, solange eine Messung läuft. + * + * Der in Phase 1 eingeführte PARTIAL_WAKE_LOCK hält nur die CPU wach — + * der Bildschirm geht trotzdem aus. Für einen Dauertest ist das richtig + * (er läuft im Vordergrunddienst weiter), aber während der Techniker einem + * laufenden Scan zusieht, ist ein ausgehendes Display schlicht lästig: er + * muss das Handy alle 30 Sekunden antippen, oft mit Handschuhen. + * + * `FLAG_KEEP_SCREEN_ON` ist dafür der richtige Weg (kein zusätzliches + * Recht nötig, Android räumt das Flag beim Beenden der Activity selbst + * ab). Muss im UI-Thread gesetzt werden. + */ + @PluginMethod + fun setKeepAwake(call: PluginCall) { + val an = call.getBoolean("on") ?: false + try { + activity.runOnUiThread { + if (an) { + activity.window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + activity.window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + resolve(call, JSObject().put("on", an)) + } catch (e: Exception) { + call.reject("setKeepAwake: ${e.message}") + } + } + /** Öffnet die System-Ortungseinstellungen (für den Klartext-Hinweis bei `isLocationEnabled()==false`). */ @PluginMethod fun openLocationSettings(call: PluginCall) { diff --git a/src/lib/components/AmpelBadge.svelte b/src/lib/components/AmpelBadge.svelte new file mode 100644 index 0000000..a3d14f1 --- /dev/null +++ b/src/lib/components/AmpelBadge.svelte @@ -0,0 +1,64 @@ + + + + + {#if s.form === 'kreis'} + + {:else if s.form === 'dreieck'} + + {:else if s.form === 'quadrat'} + + {:else} + + {/if} + + {s.zeichen} + + + {#if text} + {s.label} + {/if} + diff --git a/src/lib/components/DeviceCard.svelte b/src/lib/components/DeviceCard.svelte index c2ee329..24a376a 100644 --- a/src/lib/components/DeviceCard.svelte +++ b/src/lib/components/DeviceCard.svelte @@ -10,6 +10,7 @@ */ import { Star, Pencil } from 'lucide-svelte'; import MeasurementResult from './MeasurementResult.svelte'; + import AmpelBadge from './AmpelBadge.svelte'; import type { Device, Measurement } from '$lib/types'; import type { Tool } from '$lib/tools/types'; @@ -122,7 +123,7 @@ {#each measurements as m (m.clientId)}
- +

{m.label}

diff --git a/src/lib/components/MeasurementResult.svelte b/src/lib/components/MeasurementResult.svelte index 31ff818..72fe1b2 100644 --- a/src/lib/components/MeasurementResult.svelte +++ b/src/lib/components/MeasurementResult.svelte @@ -2,44 +2,73 @@ /** * Stellt das `result`-Objekt einer Messung lesbar dar. * - * Skalare Werte (Latenz, Anzahl, …) kommen kompakt in eine Zeile. Arrays - * — WLAN-Netze, Traceroute-Hops — werden als echte, zeilenweise Liste - * dargestellt statt in eine endlose Komma-Zeile gequetscht. + * Drei Fälle, weil die Werkzeuge alle drei liefern: + * - Skalare (Latenz, Anzahl) → kompakte Zeile mit Klartext und Einheit + * - Arrays → zeilenweise Liste, nicht in eine Komma-Wurst gequetscht + * - Objekte / Arrays von Objekten → je Eintrag eine verdichtete Zeile + * + * Der dritte Fall fehlte: `String(v)` machte aus jedem Objekt + * „[object Object]". Die WLAN-Kanalmessung besteht genau daraus + * (`eigenesNetz`, `netze`, `empfehlung24GHz`) und war auf dem Handy damit + * unlesbar — während sie in Dolibarr als Tabelle sauber aussah. + * + * Die Bezeichnungen kommen aus `messfelder.ts`, dem Gegenstück zur + * Feldtabelle des Dolibarr-Moduls. Anders als dort ist hier nichts + * ausgeblendet: die App ist die Technikeransicht. */ + import { feldLabel, feldWert, objektZeile } from '$lib/messfelder'; let { result }: { result: Record } = $props(); - /** Leere Werte ('', null, undefined, leeres Array) ausblenden */ function isEmpty(v: unknown): boolean { if (v == null || v === '') return true; if (Array.isArray(v)) return v.length === 0; + if (typeof v === 'object') return Object.keys(v as object).length === 0; return false; } const entries = $derived(Object.entries(result).filter(([, v]) => !isEmpty(v))); - /** Skalare als eine kompakte „schlüssel: wert · schlüssel: wert"-Zeile */ + const istObjekt = (v: unknown) => typeof v === 'object' && v !== null && !Array.isArray(v); + + /** Skalare als eine kompakte Zeile */ const scalarText = $derived( entries - .filter(([, v]) => !Array.isArray(v)) - .map(([k, v]) => `${k}: ${String(v)}`) + .filter(([, v]) => !Array.isArray(v) && !istObjekt(v)) + .map(([k, v]) => `${feldLabel(k)}: ${feldWert(k, v)}`) .join(' · '), ); - /** Array-Werte je als eigene Liste */ + /** Einzelne Objekte (z.B. „eigenesNetz") als eine verdichtete Zeile */ + const objekte = $derived( + entries.filter(([, v]) => istObjekt(v)) as [string, Record][], + ); + + /** Arrays — Elemente können Text ODER Objekte sein */ const lists = $derived(entries.filter(([, v]) => Array.isArray(v)) as [string, unknown[]][]); + + function zeile(item: unknown): string { + return istObjekt(item) ? objektZeile(item as Record) : String(item); + } {#if scalarText} -

{scalarText}

+

{scalarText}

{/if} +{#each objekte as [key, obj] (key)} +

+ {feldLabel(key)}: + {objektZeile(obj)} +

+{/each} + {#each lists as [key, items] (key)}
- {key}: + {feldLabel(key)}:
    {#each items as item, i (i)} -
  • {String(item)}
  • +
  • {zeile(item)}
  • {/each}
diff --git a/src/lib/components/Toast.svelte b/src/lib/components/Toast.svelte index 606d79f..544d86f 100644 --- a/src/lib/components/Toast.svelte +++ b/src/lib/components/Toast.svelte @@ -1,5 +1,30 @@ -
+
{#each toast.items as item (item.id)} -
- {item.text} -
+ {/each}
diff --git a/src/lib/components/ToolDialog.svelte b/src/lib/components/ToolDialog.svelte index 6bcd389..2432cb7 100644 --- a/src/lib/components/ToolDialog.svelte +++ b/src/lib/components/ToolDialog.svelte @@ -2,6 +2,7 @@ import type { Tool, ToolPreview } from '$lib/tools/types'; import type { Device, Protocol } from '$lib/types'; import { debugLog } from '$lib/debuglog.svelte'; + import { scanner } from '$lib/scanner'; import { X } from 'lucide-svelte'; type LiveProgress = { done: number; total: number; found: number; foundIps?: string[] }; @@ -78,6 +79,9 @@ progress = tool.supportsProgress ? { done: 0, total: 0, found: 0 } : null; if (tool.supportsProgress) step = 'running'; busy = true; + // Bildschirm anlassen, solange die Messung läuft — sonst geht das Display + // mitten im Scan aus und der Techniker muss es wieder aufwecken. + void scanner.setKeepAwake({ on: true }).catch(() => {}); try { await onrun( { ...params }, @@ -99,6 +103,7 @@ step = 'params'; } finally { busy = false; + void scanner.setKeepAwake({ on: false }).catch(() => {}); } } diff --git a/src/lib/messfelder.ts b/src/lib/messfelder.ts new file mode 100644 index 0000000..59fbafe --- /dev/null +++ b/src/lib/messfelder.ts @@ -0,0 +1,185 @@ +/** + * Klartext-Bezeichnungen und Einheiten für Messergebnis-Felder. + * + * Gegenstück zu `netdiagKundenfelder()` im Dolibarr-Modul + * (dolibarr-modul/netdiag/lib/netdiag.lib.php). **Wird dort ein Feld ergänzt, + * gehört es auch hierher** — und umgekehrt. + * + * Ein wichtiger Unterschied zum Modul: dort ist die Tabelle eine WHITELIST + * (was nicht drinsteht, erscheint nicht im Kundendokument). Hier nicht — die + * App ist die Technikeransicht, dort darf kein Messwert verschwinden. + * Unbekannte Schlüssel werden lesbar aufbereitet statt weggelassen. + */ + +type Feld = { label: string; einheit?: string }; + +const FELDER: Record = { + // gemeinsam + host: { label: 'Ziel' }, + ziel: { label: 'Ziel' }, + subnet: { label: 'Netzbereich' }, + count: { label: 'Gefundene Geräte' }, + ip: { label: 'Gerät' }, + // Ping / Laufzeit + gesendet: { label: 'Gesendet', einheit: 'Pakete' }, + empfangen: { label: 'Empfangen', einheit: 'Pakete' }, + verlustProzent: { label: 'Paketverlust', einheit: '%' }, + minMs: { label: 'Kürzeste Antwortzeit', einheit: 'ms' }, + avgMs: { label: 'Mittlere Antwortzeit', einheit: 'ms' }, + medianMs: { label: 'Typische Antwortzeit', einheit: 'ms' }, + maxMs: { label: 'Längste Antwortzeit', einheit: 'ms' }, + p95Ms: { label: 'Antwortzeit (95 %)', einheit: 'ms' }, + jitterMs: { label: 'Schwankung', einheit: 'ms' }, + messaufschlagMs: { label: 'Messaufschlag', einheit: 'ms' }, + verfahren: { label: 'Messverfahren' }, + // Durchsatz + downMbps: { label: 'Download', einheit: 'Mbit/s' }, + upMbps: { label: 'Upload', einheit: 'Mbit/s' }, + downloadMbps: { label: 'Download', einheit: 'Mbit/s' }, + uploadMbps: { label: 'Upload', einheit: 'Mbit/s' }, + gegenstelle: { label: 'Gegenstelle' }, + // Dauertest + dauerSekunden: { label: 'Messdauer', einheit: 's' }, + intervallSek: { label: 'Messabstand', einheit: 's' }, + laengsterAusfallSek: { label: 'Längster Ausfall', einheit: 's' }, + // Netz / DHCP / Dose + server: { label: 'DHCP-Server' }, + dhcpServer: { label: 'DHCP-Server' }, + lease: { label: 'Lease-Dauer', einheit: 's' }, + leaseSec: { label: 'Lease-Dauer', einheit: 's' }, + gateway: { label: 'Gateway' }, + dns: { label: 'DNS-Server' }, + ipv4: { label: 'IP-Adresse' }, + prefixLength: { label: 'Präfixlänge' }, + linkSpeedMbps: { label: 'Verbindungsgeschwindigkeit', einheit: 'Mbit/s' }, + rxLinkSpeedMbps: { label: 'Empfangsgeschwindigkeit', einheit: 'Mbit/s' }, + raum: { label: 'Raum / Dose' }, + iface: { label: 'Schnittstelle' }, + // Ports / Konflikte + scanned: { label: 'Geprüfte Ports' }, + open: { label: 'Offene Ports' }, + geprueft: { label: 'Geprüfte Adressen' }, + runden: { label: 'Messrunden' }, + konflikte: { label: 'IP-Konflikte' }, + // SNMP + port: { label: 'Switch-Port' }, + linkSpeed: { label: 'Verbindungsgeschwindigkeit' }, + eingangsFehler: { label: 'Fehler eingehend' }, + ausgangsFehler: { label: 'Fehler ausgehend' }, + sysDescr: { label: 'Gerätebeschreibung' }, + uptimeSek: { label: 'Betriebszeit', einheit: 's' }, + // Traceroute + reachedTarget: { label: 'Ziel erreicht' }, + zielErreicht: { label: 'Ziel erreicht' }, + hops: { label: 'Zwischenstellen' }, + // IP-Scan (Technikerfelder — im Kundendokument bewusst ausgeblendet) + probed: { label: 'Geprüfte Adressen' }, + answered: { label: 'Haben geantwortet' }, + arpAvailable: { label: 'ARP-Tabelle lesbar' }, + durationMs: { label: 'Dauer', einheit: 'ms' }, + abgebrochen: { label: 'Abgebrochen' }, + mdnsOk: { label: 'mDNS-Suche' }, + quelle: { label: 'Quelle des Netzbereichs' }, + neu: { label: 'Neu hinzugekommen' }, + nichtMehrErreichbar: { label: 'Nicht mehr erreichbar' }, + // WLAN + anzahlNetze: { label: 'Sichtbare WLAN-Netze' }, + eigenesNetz: { label: 'Eigenes Netz' }, + empfehlung24GHz: { label: 'Kanalempfehlung 2,4 GHz' }, + netze: { label: 'WLAN-Netze' }, + warnungen: { label: 'Hinweise' }, + demodaten: { label: 'DEMODATEN — keine echte Messung' }, + ssid: { label: 'WLAN-Name' }, + kanal: { label: 'Kanal' }, + band: { label: 'Band' }, + rssi: { label: 'Pegel', einheit: 'dBm' }, + breiteMhz: { label: 'Kanalbreite', einheit: 'MHz' }, + standard: { label: 'Standard' }, + sicherheit: { label: 'Verschlüsselung' }, + bewertung: { label: 'Bewertung' }, + stoerlast: { label: 'Störlast' }, + // Fehler / Hinweise + fehler: { label: 'Fehler' }, + hinweis: { label: 'Hinweis' }, + kundenhinweis: { label: 'Hinweis' }, +}; + +/** Sekundenwert lesbar machen (aus 8123456 wird „94 Tage 1 Std") */ +export function dauerLesbar(sek: number): string { + if (sek < 60) return `${sek} s`; + if (sek < 3600) return `${Math.round(sek / 60)} min`; + if (sek < 86400) return `${Math.floor(sek / 3600)} Std ${Math.round((sek % 3600) / 60)} min`; + return `${Math.floor(sek / 86400)} Tage ${Math.round((sek % 86400) / 3600)} Std`; +} + +/** + * Feldbezeichnung. Unbekannte Schlüssel werden nicht verworfen (anders als im + * Kundendokument), sondern aus dem camelCase-Namen lesbar gemacht — so sieht + * der Techniker auch Werte neuer Werkzeuge, bevor die Tabelle gepflegt ist. + */ +export function feldLabel(key: string): string { + const f = FELDER[key]; + if (f) return f.label; + const mitLuecken = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); + return mitLuecken.charAt(0).toUpperCase() + mitLuecken.slice(1); +} + +/** Wert mit Einheit, Sekunden lesbar, Wahrheitswerte auf Deutsch */ +export function feldWert(key: string, value: unknown): string { + const f = FELDER[key]; + if (typeof value === 'boolean') return value ? 'ja' : 'nein'; + if (value === null || value === undefined || value === '') return ''; + if (f?.einheit === 's' && typeof value === 'number' && value >= 3600) { + return dauerLesbar(value); + } + return `${value}${f?.einheit ? ' ' + f.einheit : ''}`; +} + +/** + * Ein Objekt (z.B. ein WLAN-Netz aus der Kanalmessung) zu einer Zeile + * verdichten: „Gäste-WLAN · Kanal 6 · 2.4 GHz · 20 MHz · −71 dBm". + * Ohne das stünde dort `[object Object]`. + */ +export function objektZeile(o: Record): string { + return Object.entries(o) + .filter(([, v]) => v !== null && v !== undefined && v !== '') + .map(([k, v]) => { + // Bei sprechenden Feldern (Name, Kanal, Pegel) reicht der reine Wert — + // „Kanal 6" statt „Kanal: 6" liest sich in einer Zeile flüssiger. + if (k === 'ssid' || k === 'name') return String(v); + if (k === 'kanal' || k === 'channel') return `Kanal ${v}`; + return feldWert(k, v); + }) + .filter((s) => s !== '') + .join(' · '); +} + +/** + * Klarname eines Werkzeugs für die Anzeige. + * + * `getTool(id)` findet nur registrierte Werkzeuge. Messungen aus eigenen + * Routen (WLAN-Kanalanalyse, Geräte-Monitor, IP-Test) haben keinen Eintrag in + * der Registry — dort stand bisher die rohe ID („wifikanal"). Gegenstück zu + * `netdiagToolName()` im Dolibarr-Modul. + */ +const TOOL_NAMEN: Record = { + ipscan: 'IP-Scanner', + portscan: 'Port-Scan', + ping: 'Ping / Qualität', + ipconflict: 'IP-Konflikt', + snmp: 'SNMP-Switch', + traceroute: 'Traceroute', + stresstest: 'Dauer-/Stresstest', + iperf: 'Durchsatz-Test', + wifikanal: 'WLAN-Kanalanalyse', + monitor: 'Geräte-Monitor', + iptest: 'IP-Test (Dose)', + wifitrack: 'WLAN-Empfang', + // nicht mehr vorhandene Werkzeuge, kommen in Altprotokollen noch vor + dhcpcheck: 'DHCP-Prüfung', + wifiscan: 'WLAN-Scan', +}; + +export function werkzeugName(id: string): string { + return TOOL_NAMEN[id] ?? id; +} diff --git a/src/lib/scanner.ts b/src/lib/scanner.ts index 355cb85..d6ad2e1 100644 --- a/src/lib/scanner.ts +++ b/src/lib/scanner.ts @@ -255,6 +255,12 @@ export interface NetDiagScannerPlugin { }>; /** Öffnet die System-Ortungseinstellungen — für den Hinweis, wenn WLAN-Scan wegen ausgeschalteter Ortung leer bleibt (vor Android 13) */ openLocationSettings(): Promise<{ ok: boolean }>; + /** + * Bildschirm anlassen, solange eine Messung läuft. Der WakeLock aus Phase 1 + * hält nur die CPU wach — das Display geht trotzdem aus, und der Techniker + * müsste beim Zusehen ständig antippen. + */ + setKeepAwake(opts: { on: boolean }): Promise<{ on: boolean }>; /** DHCP-Lease-Info des Geräts (Server, Lease-Dauer, Gateway, DNS) */ dhcpInfo(): Promise; /** SNMP v2c Abfrage (Switch: Link-Speed, Fehlerzähler) */ @@ -644,6 +650,9 @@ const mock: NetDiagScannerPlugin = { async openLocationSettings() { return { ok: true }; }, + async setKeepAwake(opts) { + return { on: opts.on }; + }, async dhcpInfo() { return { server: '192.168.1.1', diff --git a/src/lib/toast.svelte.ts b/src/lib/toast.svelte.ts index 17dcc1d..54e11b7 100644 --- a/src/lib/toast.svelte.ts +++ b/src/lib/toast.svelte.ts @@ -17,7 +17,16 @@ class ToastState { private lastKey = ''; private lastAt = 0; - show(text: string, type: ToastType = 'info', durationMs = 3000): void { + /** + * @param durationMs Anzeigedauer; ohne Angabe bleiben FEHLER deutlich länger + * stehen als Erfolgsmeldungen. Eine Fehlermeldung nach 3 s wegzublenden + * heißt in der Praxis, dass der Techniker sie verpasst, während er noch + * auf den Schaltschrank schaut — und dann nicht weiß, warum die Messung + * nichts geliefert hat. Fehler lassen sich außerdem antippen und dadurch + * sofort schließen (siehe +layout.svelte). + */ + show(text: string, type: ToastType = 'info', durationMs?: number): void { + const dauer = durationMs ?? (type === 'error' ? 12000 : 3000); const key = `${type}:${text}`; const now = Date.now(); // Gleiche Meldung binnen 2 s nicht erneut zeigen @@ -29,7 +38,12 @@ class ToastState { this.items = [...this.items, { id, text, type }]; setTimeout(() => { this.items = this.items.filter((t) => t.id !== id); - }, durationMs); + }, dauer); + } + + /** Meldung von Hand schließen (Antippen) */ + dismiss(id: number): void { + this.items = this.items.filter((t) => t.id !== id); } } diff --git a/src/routes/protokoll/[id]/+page.svelte b/src/routes/protokoll/[id]/+page.svelte index 055244e..9125d67 100644 --- a/src/routes/protokoll/[id]/+page.svelte +++ b/src/routes/protokoll/[id]/+page.svelte @@ -7,6 +7,7 @@ import AppHeader from '$lib/components/AppHeader.svelte'; import ToolDialog from '$lib/components/ToolDialog.svelte'; import MeasurementResult from '$lib/components/MeasurementResult.svelte'; + import AmpelBadge from '$lib/components/AmpelBadge.svelte'; import DeviceCard from '$lib/components/DeviceCard.svelte'; import TextPromptDialog from '$lib/components/TextPromptDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; @@ -24,6 +25,7 @@ import { toast } from '$lib/toast.svelte'; import { pushOverlay } from '$lib/overlay.svelte'; import { TOOLS, getTool } from '$lib/tools'; + import { werkzeugName } from '$lib/messfelder'; import type { Tool } from '$lib/tools/types'; import type { Device, Protocol } from '$lib/types'; import * as Icons from 'lucide-svelte'; @@ -46,6 +48,18 @@ const protocolTools = TOOLS.filter((t) => t.scope === 'protocol' && t.id !== 'stresstest'); const deviceTools = TOOLS.filter((t) => t.scope === 'device'); + /** Uhrzeit einer Messung (Datum nur, wenn sie nicht von heute ist) */ + function fmtUhrzeit(ts: number): string { + const d = new Date(ts); + const heute = new Date(); + const gleicherTag = + d.getDate() === heute.getDate() && + d.getMonth() === heute.getMonth() && + d.getFullYear() === heute.getFullYear(); + return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + + (gleicherTag ? '' : ' · ' + d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })); + } + /** IP-Adressen numerisch vergleichen (nicht als Text — "…9" vor "…10") */ function ipCompare(a: string, b: string): number { const pa = a.split('.').map(Number); @@ -381,8 +395,11 @@ {#each protocolMeasurements() as m (m.clientId)}
- - {getTool(m.tool)?.name ?? m.tool} + + {getTool(m.tool)?.name ?? werkzeugName(m.tool)} + + {fmtUhrzeit(m.dateMeasure)}

{m.label}

@@ -467,7 +484,12 @@
-
+ +