From 85e68ca5281f4521c52f8f94d3a4e3677dfaa169 Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Sun, 16 Aug 2026 19:09:19 +0200 Subject: [PATCH] Schnellstart, Sprung zum Ergebnis und Parametergedaechtnis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Werkzeuge waren zu umstaendlich, um sie beim Kunden wirklich zu benutzen: die Produktivdaten zeigen 19 Protokolle, aber nur 26 Messungen, fast durchweg ein einzelner IP-Scan. Der Standardfall "IP-Scan im aktuellen Netz" kostete drei Tipper, obwohl das einzige Feld ausdruecklich leer bleiben darf. - Kurzer Tap auf eine Werkzeugkarte startet sofort, langes Halten oeffnet die Optionen. Auf den betroffenen Karten steht "Tippen startet · Halten fuer Optionen" - sonst ist das Verhalten unsichtbar und der erste versehentliche Scan ueberrascht. - quickRun wird je Werkzeug AUSDRUECKLICH gesetzt, nicht hergeleitet: ToolParamField kennt kein 'required', die Pflicht steht allein im run()-Rumpf. portscan.ports hat keinen Vorgabewert und ist optional, iperf.host hat ebenfalls keinen und ist Pflicht - eine Heuristik ueber "hat Default" wuerde genau falsch herum raten. iperf bleibt deshalb bewusst ohne Schnellstart. - Der Lauf bleibt zwingend im ToolDialog: dort sitzen Bildschirmsperre, Fortschritt mit Abbrechen und die EINZIGE Fehlerbehandlung eines Werkzeuglaufs (runTool selbst hat kein try/catch). Ein Schnellstart daran vorbei wuerde Fehler verschlucken. - Die Vorschau wird nicht uebersprungen, sondern nur ohne Warnung automatisch durchgewunken. Der IP-Scan warnt ab ~1000 Adressen - genau der Fall, in dem ein versehentlicher Tap sonst minutenlang das falsche Netz absucht. - Langdruck bricht ab, sobald der Finger mehr als 10 px wandert: das Werkzeug-Raster liegt in einem Scroll-Bereich, sonst oeffnet jeder Wisch nach einer halben Sekunde den Dialog. - Nach einer Messung wird zum Ergebnis gescrollt und es 2,5 s hervorgehoben. Bei 46 Geraeten liegt die neue Karte sonst dutzende Bildschirme entfernt. block:'center', weil die feste Aktionsleiste den unteren Rand verdeckt; bei einer Geraete-Messung wird die Geraetekarte angesteuert, denn dort hinein wird gerendert. - Verwendete Parameter stehen in der Messkarte. Seit dem Schnellstart laeuft eine Messung auch ohne Dialog - dann ist das der einzige Ort, an dem man sieht, WOMIT gemessen wurde. Beschriftung aus tool.params, nicht aus der Ergebnistabelle: 'count' heisst dort "Gefundene Geraete", als Ping-Parameter aber "Anzahl Pakete". - Zuletzt benutzte Parameter werden gemerkt (neues Modul toolparams.ts). Ortsgebundene Schluessel wie 'subnet' ausdruecklich NICHT: ein aus dem vorigen Kundennetz uebernommener Netzbereich waere schlimmer als gar keiner, der Schnellstart wuerde stillschweigend das falsche Netz scannen. Im Emulator geprueft, und dabei zwei Fehler gefunden, die im Code nicht auffielen: die Parameterzeile wiederholte "Ziel: 8.8.8.8" direkt ueber dem Ergebnis (jetzt werden Werte ausgelassen, die das Ergebnis ohnehin nennt), und Traceroute schrieb "1 Hops" ins Kundendokument (jetzt "1 Station"). --- src/lib/components/ToolDialog.svelte | 46 +++++- src/lib/toolparams.ts | 88 +++++++++++ src/lib/tools/internet/iperf.ts | 3 + src/lib/tools/netzwerk/ipconflict.ts | 2 + src/lib/tools/netzwerk/ipscan.ts | 2 + src/lib/tools/netzwerk/ping.ts | 2 + src/lib/tools/netzwerk/portscan.ts | 2 + src/lib/tools/netzwerk/snmp.ts | 2 + src/lib/tools/netzwerk/traceroute.ts | 10 +- src/lib/tools/types.ts | 14 ++ src/routes/protokoll/[id]/+page.svelte | 195 +++++++++++++++++++++++-- 11 files changed, 346 insertions(+), 20 deletions(-) create mode 100644 src/lib/toolparams.ts diff --git a/src/lib/components/ToolDialog.svelte b/src/lib/components/ToolDialog.svelte index 2432cb7..7ebcff1 100644 --- a/src/lib/components/ToolDialog.svelte +++ b/src/lib/components/ToolDialog.svelte @@ -11,12 +11,24 @@ tool, protocol, device = undefined, + autostart = false, + startParams = undefined, onclose, onrun, }: { tool: Tool; protocol: Protocol; device?: Device; + /** + * true = die Messung startet ohne Zutun (Schnellstart per kurzem Tap). + * Der Lauf bleibt trotzdem in diesem Dialog: hier sitzen die + * Bildschirmsperre, der Fortschritt mit Abbrechen-Knopf und die einzige + * Fehlerbehandlung eines Werkzeuglaufs. Ein Schnellstart, der daran + * vorbeiliefe, würde Fehler verschlucken und den Abbruch unmöglich machen. + */ + autostart?: boolean; + /** vorbelegte Werte (zuletzt benutzt), überschreiben die Vorgaben */ + startParams?: Record; onclose: () => void; onrun: ( params: Record, @@ -24,10 +36,11 @@ ) => Promise; } = $props(); - // Parameter mit Vorgabewerten füllen - let params = $state>( - Object.fromEntries(tool.params.map((p) => [p.key, p.default ?? ''])), - ); + // Parameter mit Vorgabewerten füllen, dann die zuletzt benutzten drüber + let params = $state>({ + ...Object.fromEntries(tool.params.map((p) => [p.key, p.default ?? ''])), + ...(startParams ?? {}), + }); let busy = $state(false); let error = $state(''); @@ -111,6 +124,31 @@ cancelRequested = true; cancelling = true; } + + /** + * Schnellstart: sofort loslaufen, ohne dass jemand „Weiter" drücken muss. + * + * Der Vorschau-Schritt wird NICHT übersprungen, sondern nur dann automatisch + * durchgewinkt, wenn er keine Warnung enthält. Der IP-Scan warnt ab etwa + * 1000 Adressen — genau der Fall, in dem ein versehentlicher Tap sonst + * minutenlang das falsche Netz absuchen würde. Bei Warnung bleibt der Dialog + * in der Vorschau stehen und wartet auf eine bewusste Bestätigung. + */ + $effect(() => { + if (!autostart || autostartErledigt) return; + autostartErledigt = true; + void (async () => { + if (!tool.preview) { + await start(); + return; + } + await showPreviewOrRun(); + if (step === 'preview' && !previewInfo?.warning) await start(); + })(); + }); + + /** verhindert, dass der Effekt bei jeder Zustandsänderung erneut startet */ + let autostartErledigt = false;
; + +let gemerkt: Record = {}; + +export async function ladeParameterGedaechtnis(): Promise { + try { + const { value } = await Preferences.get({ key: KEY }); + gemerkt = value ? JSON.parse(value) : {}; + } catch { + // Beschädigter Eintrag darf die Seite nicht aufhalten — dann eben ohne + // Gedächtnis weiterarbeiten. + gemerkt = {}; + } +} + +/** Zuletzt benutzte Werte eines Werkzeugs (leer, wenn noch nie benutzt) */ +export function letzteParameter(toolId: string): ParamSatz { + return gemerkt[toolId] ?? {}; +} + +export async function merkeParameterFuer( + toolId: string, + params: ParamSatz, +): Promise { + const behalten: ParamSatz = {}; + for (const [k, v] of Object.entries(params)) { + if (FLUECHTIG.has(k)) continue; + if (v === '' || v === null || v === undefined) continue; + behalten[k] = v; + } + if (Object.keys(behalten).length === 0) return; + + gemerkt = { ...gemerkt, [toolId]: behalten }; + try { + await Preferences.set({ key: KEY, value: JSON.stringify(gemerkt) }); + } catch { + // Dann gilt das Gedächtnis nur für diese Sitzung. + } +} + +/** + * Startwerte für den Parameterdialog: Vorgabe des Werkzeugs, überschrieben + * von den zuletzt benutzten Werten. + * + * Es werden nur Schlüssel übernommen, die das Werkzeug HEUTE noch kennt — + * sonst schleppt ein einmal entfernter Parameter sich ewig mit und taucht in + * `Measurement.params` wieder auf, wo er im Kundendokument als unbekanntes + * Feld landet. + */ +export function startParameter(tool: Tool): ParamSatz { + const aus = Object.fromEntries( + (tool.params ?? []).map((p) => [p.key, p.default ?? '']), + ) as ParamSatz; + const alt = letzteParameter(tool.id); + for (const p of tool.params ?? []) { + if (alt[p.key] !== undefined) aus[p.key] = alt[p.key]; + } + return aus; +} diff --git a/src/lib/tools/internet/iperf.ts b/src/lib/tools/internet/iperf.ts index aaf47bd..9c8e259 100644 --- a/src/lib/tools/internet/iperf.ts +++ b/src/lib/tools/internet/iperf.ts @@ -19,6 +19,9 @@ export const iperfTool: Tool = { icon: 'gauge-circle', description: 'Misst Down-/Upload-Bandbreite gegen eine Gegenstelle.', scope: 'protocol', + // BEWUSST kein quickRun: `host` hat keinen sinnvollen Vorgabewert (die + // Gegenstelle ist je Einsatz eine andere) und run() bricht ohne ihn sofort + // ab. Ein Schnellstart würde beim Kunden nur eine Fehlermeldung erzeugen. params: [ { key: 'host', label: 'Gegenstelle (IP)', type: 'text', placeholder: '192.168.1.20' }, { key: 'port', label: 'Port', type: 'number', default: 5201 }, diff --git a/src/lib/tools/netzwerk/ipconflict.ts b/src/lib/tools/netzwerk/ipconflict.ts index 8fab2ed..398c7e1 100644 --- a/src/lib/tools/netzwerk/ipconflict.ts +++ b/src/lib/tools/netzwerk/ipconflict.ts @@ -17,6 +17,8 @@ export const ipConflictTool: Tool = { icon: 'alert-triangle', description: 'Findet IP-Adressen, die zwei Geräte gleichzeitig benutzen.', scope: 'protocol', + // Netzbereich optional, Runden haben einen Vorgabewert + quickRun: true, params: [ { key: 'subnet', diff --git a/src/lib/tools/netzwerk/ipscan.ts b/src/lib/tools/netzwerk/ipscan.ts index 90c209a..9abb54b 100644 --- a/src/lib/tools/netzwerk/ipscan.ts +++ b/src/lib/tools/netzwerk/ipscan.ts @@ -159,6 +159,8 @@ export const ipScanTool: Tool = { description: 'Sucht Geräte per Ping, Port-Probe, ARP-Abgleich und mDNS/Bonjour.', scope: 'protocol', supportsProgress: true, + // Netzbereich darf leer bleiben (wird ueber den Adapter aufgeloest) + quickRun: true, params: [ { key: 'subnet', diff --git a/src/lib/tools/netzwerk/ping.ts b/src/lib/tools/netzwerk/ping.ts index 7be751a..fcb491c 100644 --- a/src/lib/tools/netzwerk/ping.ts +++ b/src/lib/tools/netzwerk/ping.ts @@ -13,6 +13,8 @@ export const pingTool: Tool = { icon: 'activity', description: 'Misst Latenz, Jitter und Paketverlust.', scope: 'device', + // Ziel kommt aus dem angetippten Geraet, Paketzahl hat Vorgabewert + quickRun: true, params: [ { key: 'host', label: 'Ziel (leer = Gerät)', type: 'text', placeholder: '192.168.1.1' }, { key: 'count', label: 'Anzahl Pakete', type: 'number', default: 20 }, diff --git a/src/lib/tools/netzwerk/portscan.ts b/src/lib/tools/netzwerk/portscan.ts index 5d7bf90..aeaa06a 100644 --- a/src/lib/tools/netzwerk/portscan.ts +++ b/src/lib/tools/netzwerk/portscan.ts @@ -16,6 +16,8 @@ export const portScanTool: Tool = { icon: 'scan-line', description: 'Prüft offene TCP-Ports eines einzelnen Geräts.', scope: 'device', + // Leere Portliste bedeutet ausdruecklich DEFAULT_PORTS + quickRun: true, params: [ { key: 'ports', diff --git a/src/lib/tools/netzwerk/snmp.ts b/src/lib/tools/netzwerk/snmp.ts index ab48e0d..6a9bb68 100644 --- a/src/lib/tools/netzwerk/snmp.ts +++ b/src/lib/tools/netzwerk/snmp.ts @@ -23,6 +23,8 @@ export const snmpTool: Tool = { icon: 'network', description: 'Liest Link-Speed und Fehlerzähler eines Switches (SNMP v2c).', scope: 'device', + // Community hat den Vorgabewert 'public', Host kommt aus dem Geraet + quickRun: true, params: [ { key: 'host', label: 'Switch-IP (leer = Gerät)', type: 'text', placeholder: '192.168.1.10' }, { key: 'community', label: 'SNMP Community', type: 'text', default: 'public' }, diff --git a/src/lib/tools/netzwerk/traceroute.ts b/src/lib/tools/netzwerk/traceroute.ts index 5d264ed..951234f 100644 --- a/src/lib/tools/netzwerk/traceroute.ts +++ b/src/lib/tools/netzwerk/traceroute.ts @@ -12,6 +12,8 @@ export const tracerouteTool: Tool = { icon: 'route', description: 'Zeigt die Netzwerk-Hops bis zum Ziel.', scope: 'protocol', + // Ziel hat mit 8.8.8.8 einen brauchbaren Vorgabewert + quickRun: true, params: [{ key: 'host', label: 'Ziel', type: 'text', default: '8.8.8.8' }], async run(ctx) { const host = String(ctx.params.host || '8.8.8.8'); @@ -25,12 +27,16 @@ export const tracerouteTool: Tool = { if (!reachedTarget) status = 2; if (antwortend === 0) status = 3; // gar keine Antwort -> nicht messbar + // Einzahl/Mehrzahl: bei einem direkt erreichbaren Ziel stand hier „1 Hops". + // Das Label landet unveraendert im Kundendokument. + const stationen = (n: number) => `${n} ${n === 1 ? 'Station' : 'Stationen'}`; + return { label: reachedTarget - ? `${hops.length} Hops bis ${host}` + ? `${stationen(hops.length)} bis ${host}` : antwortend === 0 ? `${host}: keine Antwort auf dem Weg` - : `${host} nicht erreicht (${antwortend} Hops geantwortet)`, + : `${host} nicht erreicht (${stationen(antwortend)} geantwortet)`, result: { ziel: host, zielErreicht: reachedTarget, diff --git a/src/lib/tools/types.ts b/src/lib/tools/types.ts index ebd009e..9cfde42 100644 --- a/src/lib/tools/types.ts +++ b/src/lib/tools/types.ts @@ -97,6 +97,20 @@ export interface Tool { params: ToolParamField[]; /** true = das Tool meldet Fortschritt über `ctx.onProgress` — ToolDialog zeigt dann Fortschrittsanzeige + Abbrechen statt des einfachen Spinners */ supportsProgress?: boolean; + /** + * true = ein kurzer Tap auf die Werkzeugkarte startet die Messung sofort mit + * den zuletzt benutzten Werten; der Parameterdialog kommt erst beim langen + * Drücken. + * + * MUSS je Werkzeug ausdrücklich gesetzt werden und darf NICHT aus den + * Parametern hergeleitet werden: `ToolParamField` kennt kein `required`, ob + * ein Parameter Pflicht ist, steht allein im `run()`-Rumpf. `portscan.ports` + * hat keinen Vorgabewert und ist trotzdem optional, `iperf.host` hat + * ebenfalls keinen und ist Pflicht — eine Heuristik über „hat Default" + * würde also genau falsch herum raten und beim Kunden eine Messung + * losschicken, die sofort mit „Keine Gegenstelle angegeben" abbricht. + */ + quickRun?: boolean; /** Optionale Vorschau vor dem Start (z.B. IP-Scan: Adapter/eigene IP/Maske/Gateway/Anzahl) */ preview?(ctx: ToolContext): Promise; run(ctx: ToolContext): Promise; diff --git a/src/routes/protokoll/[id]/+page.svelte b/src/routes/protokoll/[id]/+page.svelte index 2a8653e..841e8e3 100644 --- a/src/routes/protokoll/[id]/+page.svelte +++ b/src/routes/protokoll/[id]/+page.svelte @@ -1,5 +1,5 @@