Phase 6: Lesbarkeit und Bedienung — Ampel mit Form, Klartext-Werte, Toasts
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.
This commit is contained in:
parent
c98f2b1152
commit
69a4c50711
10 changed files with 416 additions and 21 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
64
src/lib/components/AmpelBadge.svelte
Normal file
64
src/lib/components/AmpelBadge.svelte
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<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>
|
||||
|
|
@ -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)}
|
||||
<div class="mt-1.5 flex items-start gap-2 border-t border-zinc-800 pt-1.5">
|
||||
<span class="mt-1 h-2 w-2 shrink-0 rounded-full {ampelDot[m.measureStatus]}"></span>
|
||||
<span class="mt-0.5"><AmpelBadge status={m.measureStatus} size={12} /></span>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs {ampel[m.measureStatus]}">{m.label}</p>
|
||||
<MeasurementResult result={m.result} />
|
||||
|
|
|
|||
|
|
@ -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<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)));
|
||||
|
||||
/** 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<string, unknown>][],
|
||||
);
|
||||
|
||||
/** 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, unknown>) : String(item);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if scalarText}
|
||||
<p class="break-words text-[11px] text-zinc-500">{scalarText}</p>
|
||||
<p class="break-words text-xs text-zinc-400 tabular-nums">{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-[11px] text-zinc-600">{key}:</span>
|
||||
<span class="text-xs text-zinc-500">{feldLabel(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-[11px] text-zinc-400">{String(item)}</li>
|
||||
<li class="break-words pl-2 text-xs text-zinc-400">{zeile(item)}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,30 @@
|
|||
<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',
|
||||
|
|
@ -8,10 +33,21 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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)"
|
||||
>
|
||||
{#each toast.items as item (item.id)}
|
||||
<div class="rounded-lg px-4 py-2 text-sm text-white shadow-lg {colors[item.type]}">
|
||||
{item.text}
|
||||
</div>
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
185
src/lib/messfelder.ts
Normal file
185
src/lib/messfelder.ts
Normal file
|
|
@ -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<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;
|
||||
}
|
||||
|
|
@ -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<DhcpLease>;
|
||||
/** 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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
<div class="mb-1.5 rounded-lg bg-zinc-900 p-2.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<p class="mt-1 text-xs {ampel[m.measureStatus]}">{m.label}</p>
|
||||
<div class="mt-0.5"><MeasurementResult result={m.result} /></div>
|
||||
|
|
@ -467,7 +484,12 @@
|
|||
</div>
|
||||
|
||||
<!-- Abschluss-Leiste -->
|
||||
<div class="fixed inset-x-0 bottom-0 border-t border-zinc-800 bg-zinc-900 p-3 safe-bottom">
|
||||
<!-- 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"
|
||||
>
|
||||
<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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue