Compare commits

..

No commits in common. "295ccb2e62a49858d8961890abcfef2108c20c65" and "c98f2b11522f9fdab2cea459d25ac4107189105f" have entirely different histories.

10 changed files with 21 additions and 416 deletions

View file

@ -1007,36 +1007,6 @@ 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) {

View file

@ -1,64 +0,0 @@
<script lang="ts">
/**
* Ampel-Anzeige einer Messung — mit Form UND Zeichen, nicht nur Farbe.
*
* Warum: Rot-Grün-Sehschwäche betrifft rund 9 % der Männer, und auf einer
* Baustelle im Sonnenlicht sind gesättigte Farben auf dem Handy ohnehin
* kaum auseinanderzuhalten. Vorher unterschieden sich die vier Zustände
* ausschließlich durch die Füllfarbe eines Punktes — bei einem Ausdruck in
* Graustufen (Kunde druckt das PDF) fiel der Unterschied ganz weg.
*
* Deshalb bekommt jeder Zustand zusätzlich eine eigene Form und ein Zeichen:
* ok Kreis ✓
* Warnung Dreieck !
* Fehler Quadrat ✕
* nicht messbar Raute ?
*/
import type { MeasureStatus } from '$lib/types';
let {
status,
text = false,
size = 14,
}: {
status: MeasureStatus;
/** zusätzlich den Klartext danebenstellen */
text?: boolean;
size?: number;
} = $props();
const STUFEN = [
{ label: 'OK', zeichen: '✓', farbe: 'text-emerald-400', form: 'kreis' },
{ label: 'Warnung', zeichen: '!', farbe: 'text-amber-300', form: 'dreieck' },
{ label: 'Fehler', zeichen: '✕', farbe: 'text-red-400', form: 'quadrat' },
{ label: 'Nicht messbar', zeichen: '?', farbe: 'text-zinc-300', form: 'raute' },
] as const;
const s = $derived(STUFEN[status] ?? STUFEN[1]);
</script>
<span class="inline-flex shrink-0 items-center gap-1 {s.farbe}" title={s.label}>
<svg width={size} height={size} viewBox="0 0 16 16" role="img" aria-label={s.label}>
{#if s.form === 'kreis'}
<circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.5" />
{:else if s.form === 'dreieck'}
<path d="M8 1.5 L15 14 L1 14 Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
{:else if s.form === 'quadrat'}
<rect x="1.5" y="1.5" width="13" height="13" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5" />
{:else}
<path d="M8 1 L15 8 L8 15 L1 8 Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
{/if}
<text
x="8"
y="11.5"
text-anchor="middle"
fill="currentColor"
style="font-size: 8px; font-weight: 700"
>
{s.zeichen}
</text>
</svg>
{#if text}
<span class="text-xs font-medium">{s.label}</span>
{/if}
</span>

View file

@ -10,7 +10,6 @@
*/
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';
@ -123,7 +122,7 @@
{#each measurements as m (m.clientId)}
<div class="mt-1.5 flex items-start gap-2 border-t border-zinc-800 pt-1.5">
<span class="mt-0.5"><AmpelBadge status={m.measureStatus} size={12} /></span>
<span class="mt-1 h-2 w-2 shrink-0 rounded-full {ampelDot[m.measureStatus]}"></span>
<div class="min-w-0">
<p class="text-xs {ampel[m.measureStatus]}">{m.label}</p>
<MeasurementResult result={m.result} />

View file

@ -2,73 +2,44 @@
/**
* Stellt das `result`-Objekt einer Messung lesbar dar.
*
* 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.
* 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.
*/
import { feldLabel, feldWert, objektZeile } from '$lib/messfelder';
let { result }: { result: Record<string, unknown> } = $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)));
const istObjekt = (v: unknown) => typeof v === 'object' && v !== null && !Array.isArray(v);
/** Skalare als eine kompakte Zeile */
/** Skalare als eine kompakte „schlüssel: wert · schlüssel: wert"-Zeile */
const scalarText = $derived(
entries
.filter(([, v]) => !Array.isArray(v) && !istObjekt(v))
.map(([k, v]) => `${feldLabel(k)}: ${feldWert(k, v)}`)
.filter(([, v]) => !Array.isArray(v))
.map(([k, v]) => `${k}: ${String(v)}`)
.join(' · '),
);
/** Einzelne Objekte (z.B. „eigenesNetz") als eine verdichtete Zeile */
const objekte = $derived(
entries.filter(([, v]) => istObjekt(v)) as [string, Record<string, unknown>][],
);
/** Arrays — Elemente können Text ODER Objekte sein */
/** Array-Werte je als eigene Liste */
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, unknown>) : String(item);
}
</script>
{#if scalarText}
<p class="break-words text-xs text-zinc-400 tabular-nums">{scalarText}</p>
<p class="break-words text-[11px] text-zinc-500">{scalarText}</p>
{/if}
{#each objekte as [key, obj] (key)}
<p class="mt-0.5 break-words text-xs text-zinc-400">
<span class="text-zinc-500">{feldLabel(key)}:</span>
{objektZeile(obj)}
</p>
{/each}
{#each lists as [key, items] (key)}
<div class="mt-0.5">
<span class="text-xs text-zinc-500">{feldLabel(key)}:</span>
<span class="text-[11px] text-zinc-600">{key}:</span>
<ul class="mt-0.5 flex flex-col gap-0.5">
{#each items as item, i (i)}
<li class="break-words pl-2 text-xs text-zinc-400">{zeile(item)}</li>
<li class="break-words pl-2 text-[11px] text-zinc-400">{String(item)}</li>
{/each}
</ul>
</div>

View file

@ -1,30 +1,5 @@
<script lang="ts">
/**
* Kurzmeldungen.
*
* Bewusst UNTEN statt oben: auf einem heutigen Handy ist der obere Rand
* einhändig nicht erreichbar, und genau dort liegen Statusleiste und
* Kamera-Aussparung. Eine Fehlermeldung, die man wegtippen möchte, muss im
* Daumenbereich liegen.
*
* Fehler bleiben deutlich länger stehen (siehe toast.svelte.ts) und lassen
* sich antippen, um sie sofort zu schließen — deshalb `pointer-events-auto`
* auf der Meldung selbst, während der Container durchlässig bleibt.
*/
import { toast } from '$lib/toast.svelte';
import { X } from 'lucide-svelte';
/**
* Abstand nach unten. Seiten mit fester Aktionsleiste (z.B. „Abschließen &
* synchronisieren" auf der Protokollseite) markieren diese mit
* `style="--leiste: 1"`; die Meldung rückt dann darüber, statt genau den
* Knopf zu verdecken, den man als Nächstes drücken will.
*/
let abstandUnten = $state(16);
$effect(() => {
const leiste = document.querySelector('[style*="--leiste"]');
abstandUnten = leiste ? Math.round(leiste.getBoundingClientRect().height) + 12 : 16;
});
const colors = {
info: 'bg-zinc-700',
@ -33,21 +8,10 @@
};
</script>
<div
class="pointer-events-none fixed inset-x-0 z-50 flex flex-col items-center gap-2 px-3"
style="bottom: calc(env(safe-area-inset-bottom, 0px) + {abstandUnten}px)"
>
<div class="pointer-events-none fixed inset-x-0 top-0 z-50 flex flex-col items-center gap-2 px-3 pb-3 safe-top">
{#each toast.items as item (item.id)}
<button
class="pointer-events-auto flex max-w-full items-center gap-2 rounded-lg px-4 py-2.5 text-left text-sm text-white shadow-lg {colors[
item.type
]}"
onclick={() => toast.dismiss(item.id)}
>
<span class="min-w-0">{item.text}</span>
{#if item.type === 'error'}
<X size={14} class="shrink-0 opacity-70" />
{/if}
</button>
<div class="rounded-lg px-4 py-2 text-sm text-white shadow-lg {colors[item.type]}">
{item.text}
</div>
{/each}
</div>

View file

@ -2,7 +2,6 @@
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[] };
@ -79,9 +78,6 @@
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 },
@ -103,7 +99,6 @@
step = 'params';
} finally {
busy = false;
void scanner.setKeepAwake({ on: false }).catch(() => {});
}
}

View file

@ -1,185 +0,0 @@
/**
* 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<string, Feld> = {
// 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, unknown>): 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<string, string> = {
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;
}

View file

@ -255,12 +255,6 @@ 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<DhcpLease>;
/** SNMP v2c Abfrage (Switch: Link-Speed, Fehlerzähler) */
@ -650,9 +644,6 @@ const mock: NetDiagScannerPlugin = {
async openLocationSettings() {
return { ok: true };
},
async setKeepAwake(opts) {
return { on: opts.on };
},
async dhcpInfo() {
return {
server: '192.168.1.1',

View file

@ -17,16 +17,7 @@ class ToastState {
private lastKey = '';
private lastAt = 0;
/**
* @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);
show(text: string, type: ToastType = 'info', durationMs = 3000): void {
const key = `${type}:${text}`;
const now = Date.now();
// Gleiche Meldung binnen 2 s nicht erneut zeigen
@ -38,12 +29,7 @@ class ToastState {
this.items = [...this.items, { id, text, type }];
setTimeout(() => {
this.items = this.items.filter((t) => t.id !== id);
}, dauer);
}
/** Meldung von Hand schließen (Antippen) */
dismiss(id: number): void {
this.items = this.items.filter((t) => t.id !== id);
}, durationMs);
}
}

View file

@ -7,7 +7,6 @@
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';
@ -25,7 +24,6 @@
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';
@ -48,18 +46,6 @@
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);
@ -395,11 +381,8 @@
{#each protocolMeasurements() as m (m.clientId)}
<div class="mb-1.5 rounded-lg bg-zinc-900 p-2.5">
<div class="flex items-center gap-2">
<AmpelBadge status={m.measureStatus} />
<span class="text-sm font-medium">{getTool(m.tool)?.name ?? werkzeugName(m.tool)}</span>
<!-- Zeitstempel: bei mehreren Läufen desselben Werkzeugs sonst
nicht erkennbar, welche Messung von wann ist -->
<span class="ml-auto shrink-0 text-[11px] text-zinc-500">{fmtUhrzeit(m.dateMeasure)}</span>
<span class="h-2.5 w-2.5 shrink-0 rounded-full {ampelDot[m.measureStatus]}"></span>
<span class="text-sm font-medium">{getTool(m.tool)?.name ?? m.tool}</span>
</div>
<p class="mt-1 text-xs {ampel[m.measureStatus]}">{m.label}</p>
<div class="mt-0.5"><MeasurementResult result={m.result} /></div>
@ -484,12 +467,7 @@
</div>
<!-- Abschluss-Leiste -->
<!-- Feste Aktionsleiste. Sie setzt --leiste-hoehe, damit die Kurzmeldungen
darüber erscheinen statt den Abschließen-Knopf zu verdecken. -->
<div
class="fixed inset-x-0 bottom-0 border-t border-zinc-800 bg-zinc-900 p-3 safe-bottom"
style="--leiste: 1"
>
<div class="fixed inset-x-0 bottom-0 border-t border-zinc-800 bg-zinc-900 p-3 safe-bottom">
<button
class="w-full rounded-lg bg-emerald-600 py-2.5 font-semibold text-white active:bg-emerald-700 disabled:opacity-50"
onclick={finish}