Schnellstart, Sprung zum Ergebnis und Parametergedaechtnis
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").
This commit is contained in:
parent
767861046c
commit
85e68ca528
11 changed files with 346 additions and 20 deletions
|
|
@ -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<string, string | number>;
|
||||
onclose: () => void;
|
||||
onrun: (
|
||||
params: Record<string, string | number>,
|
||||
|
|
@ -24,10 +36,11 @@
|
|||
) => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
// Parameter mit Vorgabewerten füllen
|
||||
let params = $state<Record<string, string | number>>(
|
||||
Object.fromEntries(tool.params.map((p) => [p.key, p.default ?? ''])),
|
||||
);
|
||||
// Parameter mit Vorgabewerten füllen, dann die zuletzt benutzten drüber
|
||||
let params = $state<Record<string, string | number>>({
|
||||
...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;
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
|
|||
88
src/lib/toolparams.ts
Normal file
88
src/lib/toolparams.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* Gedächtnis für zuletzt benutzte Werkzeug-Parameter.
|
||||
*
|
||||
* Zweck: Der Techniker stellt beim Kunden dieselben Werte immer wieder neu
|
||||
* ein — SNMP-Community, Anzahl Ping-Pakete, Portliste. Ohne Gedächtnis ist
|
||||
* der Schnellstart (kurzer Tap startet sofort) nur für Werkzeuge nützlich,
|
||||
* deren Vorgabewerte zufällig passen; mit Gedächtnis für alle.
|
||||
*
|
||||
* ORTSGEBUNDENE SCHLÜSSEL WERDEN BEWUSST NICHT GEMERKT (siehe FLUECHTIG).
|
||||
* Ein aus dem vorigen Kundennetz übernommener Netzbereich wäre schlimmer als
|
||||
* gar keiner: der Schnellstart würde stillschweigend das falsche Netz scannen
|
||||
* und ein leeres Ergebnis ins Abnahmeprotokoll schreiben. Bleibt das Feld
|
||||
* leer, lösen ipscan und ipconflict den Bereich ohnehin selbst über den
|
||||
* aktiven Adapter auf — das ist immer richtig.
|
||||
*/
|
||||
|
||||
import { Preferences } from '@capacitor/preferences';
|
||||
import type { Tool } from './tools/types';
|
||||
|
||||
const KEY = 'nd_toolparams';
|
||||
|
||||
/**
|
||||
* Schlüssel, die NICHT über einen Kunden hinaus gemerkt werden dürfen, weil
|
||||
* sie an Ort und Netz hängen. Bewusst eine Sperrliste statt einer Erlaubnis-
|
||||
* liste: ein neuer, harmloser Parameter soll ohne Zutun gemerkt werden, ein
|
||||
* neuer ortsgebundener fällt beim Hinzufügen auf.
|
||||
*/
|
||||
const FLUECHTIG = new Set(['subnet', 'ip', 'ziel', 'target']);
|
||||
|
||||
type ParamSatz = Record<string, string | number>;
|
||||
|
||||
let gemerkt: Record<string, ParamSatz> = {};
|
||||
|
||||
export async function ladeParameterGedaechtnis(): Promise<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<ToolPreview>;
|
||||
run(ctx: ToolContext): Promise<ToolRunResult>;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { onMount, onDestroy, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { App } from '@capacitor/app';
|
||||
|
|
@ -26,7 +26,8 @@
|
|||
import { toast } from '$lib/toast.svelte';
|
||||
import { pushOverlay } from '$lib/overlay.svelte';
|
||||
import { TOOLS, getTool } from '$lib/tools';
|
||||
import { werkzeugName } from '$lib/messfelder';
|
||||
import { werkzeugName, feldLabel } from '$lib/messfelder';
|
||||
import { ladeParameterGedaechtnis, startParameter, merkeParameterFuer } from '$lib/toolparams';
|
||||
import type { Tool } from '$lib/tools/types';
|
||||
import type { Device, Protocol } from '$lib/types';
|
||||
import * as Icons from 'lucide-svelte';
|
||||
|
|
@ -46,6 +47,12 @@
|
|||
let erkanntesSubnetz = $state('');
|
||||
/** Menü hinter den drei Punkten (selten gebrauchte, gefährliche Aktionen) */
|
||||
let ueberlaufOffen = $state(false);
|
||||
/** Schnellstart: der nächste Dialog startet ohne Zutun */
|
||||
let sofortStarten = $state(false);
|
||||
/** vorbelegte Werte für den nächsten Dialog (zuletzt benutzt) */
|
||||
let startParams = $state<Record<string, string | number> | undefined>(undefined);
|
||||
/** frisch entstandene Messung — wird kurz hervorgehoben */
|
||||
let letzteMessungId = $state<string | null>(null);
|
||||
|
||||
/** Kompakte Zusammenfassung für die zugeklappte Kopfzeile */
|
||||
const kopfZusammenfassung = $derived(
|
||||
|
|
@ -122,6 +129,9 @@
|
|||
// der Platz für die Werkzeuge frei.
|
||||
kopfdatenOffen = !(p.location || p.subnet || p.note);
|
||||
|
||||
// Zuletzt benutzte Werkzeug-Parameter laden (fuer den Schnellstart)
|
||||
await ladeParameterGedaechtnis();
|
||||
|
||||
// Erkanntes Subnetz als Eingabevorschlag bereitstellen (best effort)
|
||||
try {
|
||||
const local = await scanner.getLocalSubnet();
|
||||
|
|
@ -169,9 +179,100 @@
|
|||
return (map[pascal] ?? Icons.Wrench) as typeof Icons.Wrench;
|
||||
}
|
||||
|
||||
function openTool(tool: Tool, device?: Device) {
|
||||
/**
|
||||
* Die verwendeten Parameter einer Messung als eine Zeile.
|
||||
*
|
||||
* Die Beschriftung kommt aus `tool.params`, NICHT aus der Ergebnis-
|
||||
* Feldtabelle: derselbe Schlüssel bedeutet je nach Werkzeug etwas anderes.
|
||||
* `count` heißt im IP-Scan-Ergebnis „Gefundene Geräte", als Ping-Parameter
|
||||
* dagegen „Anzahl Pakete" — über die Ergebnistabelle stünde in der Messkarte
|
||||
* „Gefundene Geräte: 20", wo „Anzahl Pakete: 20" gemeint ist.
|
||||
*
|
||||
* Leere Werte werden weggelassen: ein bewusst leer gelassener Netzbereich
|
||||
* ist keine Angabe, sondern die Anweisung „nimm das aktuelle Netz".
|
||||
*
|
||||
* Ebenfalls weggelassen wird, was das Ergebnis ohnehin schon nennt. Beim
|
||||
* Traceroute stand sonst „Ziel: 8.8.8.8" direkt über „Ziel: 8.8.8.8 · Ziel
|
||||
* erreicht: ja" — zwei Zeilen für dieselbe Angabe, und die Messkarte ist auf
|
||||
* einem Handy der knappste Platz der ganzen App.
|
||||
*/
|
||||
function parameterZeile(m: {
|
||||
tool: string;
|
||||
params?: Record<string, unknown>;
|
||||
result?: unknown;
|
||||
}): string {
|
||||
const felder = getTool(m.tool)?.params ?? [];
|
||||
const imErgebnis = new Set(
|
||||
Object.values((m.result ?? {}) as Record<string, unknown>)
|
||||
.filter((v) => typeof v === 'string' || typeof v === 'number')
|
||||
.map((v) => String(v)),
|
||||
);
|
||||
return Object.entries(m.params ?? {})
|
||||
.filter(([, v]) => v !== '' && v !== null && v !== undefined)
|
||||
.filter(([, v]) => !imErgebnis.has(String(v)))
|
||||
.map(([k, v]) => {
|
||||
const label = felder.find((p) => p.key === k)?.label ?? feldLabel(k);
|
||||
return `${label}: ${v}`;
|
||||
})
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
function openTool(tool: Tool, device?: Device, sofort = false) {
|
||||
activeTool = tool;
|
||||
activeDevice = device;
|
||||
sofortStarten = sofort;
|
||||
startParams = startParameter(tool);
|
||||
}
|
||||
|
||||
/* --- Schnellstart: kurz tippen startet, halten öffnet die Optionen ---
|
||||
*
|
||||
* Der Standardfall beim Kunden ist „IP-Scan im aktuellen Netz" — dafür waren
|
||||
* bisher drei Tipper nötig (Karte → Weiter → Scan starten), obwohl das
|
||||
* einzige Feld ausdrücklich leer bleiben darf. Die Zahlen aus der
|
||||
* Produktivdatenbank stützen das: 19 Protokolle, aber nur 26 Messungen, fast
|
||||
* durchweg ein einzelner IP-Scan. Die Werkzeuge waren zu umständlich, um sie
|
||||
* wirklich zu benutzen.
|
||||
*
|
||||
* Der Langdruck muss abbrechen, sobald der Finger wandert: das Werkzeug-
|
||||
* Raster liegt in einem Scroll-Bereich, sonst öffnet jeder Wisch, der auf
|
||||
* einer Karte beginnt, nach einer halben Sekunde den Dialog.
|
||||
*/
|
||||
const LANGDRUCK_MS = 500;
|
||||
let druckTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let langGedrueckt = false;
|
||||
let druckStart: { x: number; y: number } | null = null;
|
||||
|
||||
function druckAn(e: PointerEvent, tool: Tool) {
|
||||
langGedrueckt = false;
|
||||
druckStart = { x: e.clientX, y: e.clientY };
|
||||
druckTimer = setTimeout(() => {
|
||||
langGedrueckt = true;
|
||||
druckTimer = null;
|
||||
openTool(tool); // Dialog mit Optionen
|
||||
}, LANGDRUCK_MS);
|
||||
}
|
||||
|
||||
function druckBewegung(e: PointerEvent) {
|
||||
if (!druckTimer || !druckStart) return;
|
||||
// 10 px Toleranz: ein ruhiger Finger zittert, ein Wisch nicht
|
||||
if (Math.hypot(e.clientX - druckStart.x, e.clientY - druckStart.y) > 10) druckAus();
|
||||
}
|
||||
|
||||
function druckAus() {
|
||||
if (druckTimer) clearTimeout(druckTimer);
|
||||
druckTimer = null;
|
||||
druckStart = null;
|
||||
}
|
||||
|
||||
function werkzeugTap(tool: Tool) {
|
||||
druckAus();
|
||||
// Der Langdruck hat den Dialog schon geöffnet — der nachfolgende Klick
|
||||
// darf ihn nicht sofort durch einen zweiten Aufruf ersetzen.
|
||||
if (langGedrueckt) {
|
||||
langGedrueckt = false;
|
||||
return;
|
||||
}
|
||||
openTool(tool, undefined, tool.quickRun ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -211,7 +312,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
addMeasurement(protocol, {
|
||||
const neueMessung = addMeasurement(protocol, {
|
||||
deviceClientId: activeDevice?.clientId ?? null,
|
||||
tool: tool.id,
|
||||
category: tool.category,
|
||||
|
|
@ -223,6 +324,36 @@
|
|||
});
|
||||
await persist();
|
||||
toast.show(`${tool.name}: ${result.label}`, result.measureStatus === 2 ? 'error' : 'success');
|
||||
void merkeParameterFuer(tool.id, params);
|
||||
await zeigeErgebnis(neueMessung.clientId, activeDevice?.clientId ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nach einer Messung zum frischen Ergebnis scrollen und es kurz hervorheben.
|
||||
*
|
||||
* Grund: bei einem Kunden mit 46 Geräten liegt die neue Karte dutzende
|
||||
* Bildschirme entfernt, und der Techniker sucht sie im Keller einhändig.
|
||||
*
|
||||
* Drei Feinheiten, die jeweils einen Fehlversuch gekostet hätten:
|
||||
* - `tick()` ist Pflicht — die Karte existiert vor dem State-Update nicht.
|
||||
* - `block: 'center'` statt 'start'/'end': die feste Aktionsleiste am
|
||||
* unteren Rand verdeckt sonst genau das Ergebnis.
|
||||
* - Bei einer Geräte-Messung wird die GERÄTE-Karte angesteuert, denn dort
|
||||
* hinein wird das Ergebnis gerendert (DeviceCard), nicht in die
|
||||
* Messungen-Liste.
|
||||
*/
|
||||
async function zeigeErgebnis(messungId: string, geraetId: string | null) {
|
||||
letzteMessungId = messungId;
|
||||
await tick();
|
||||
const ziel = geraetId
|
||||
? document.getElementById('ger-' + geraetId)
|
||||
: document.getElementById('mess-' + messungId);
|
||||
ziel?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// Hervorhebung wieder abräumen: bei mehreren Läufen desselben Werkzeugs
|
||||
// wäre sonst nicht erkennbar, welche Karte die neue ist.
|
||||
setTimeout(() => {
|
||||
if (letzteMessungId === messungId) letzteMessungId = null;
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
/** Protokoll abschließen und synchronisieren */
|
||||
|
|
@ -396,12 +527,25 @@
|
|||
{#each protocolTools as tool (tool.id)}
|
||||
{@const IconC = icon(tool.icon)}
|
||||
<button
|
||||
class="flex flex-col items-start gap-1 rounded-lg bg-zinc-800 p-3 text-left active:bg-zinc-700"
|
||||
onclick={() => openTool(tool)}
|
||||
class="flex select-none touch-manipulation flex-col items-start gap-1 rounded-lg bg-zinc-800 p-3 text-left active:bg-zinc-700"
|
||||
onclick={() => werkzeugTap(tool)}
|
||||
onpointerdown={(e) => druckAn(e, tool)}
|
||||
onpointermove={druckBewegung}
|
||||
onpointerup={druckAus}
|
||||
onpointerleave={druckAus}
|
||||
onpointercancel={druckAus}
|
||||
oncontextmenu={(e) => e.preventDefault()}
|
||||
>
|
||||
<IconC size={20} class="text-sky-400" />
|
||||
<span class="text-sm font-medium">{tool.name}</span>
|
||||
<span class="text-[11px] leading-tight text-zinc-500">{tool.description}</span>
|
||||
{#if tool.quickRun}
|
||||
<!-- Ohne diesen Hinweis ist das Verhalten unsichtbar und der
|
||||
erste versehentlich ausgeloeste Scan ueberrascht. -->
|
||||
<span class="text-[10px] leading-tight text-zinc-600">
|
||||
Tippen startet · Halten für Optionen
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
<!-- Dauer-/Stresstest: eigene Seite (Live-Diagramm + Foreground-Service) -->
|
||||
|
|
@ -467,7 +611,12 @@
|
|||
<section class="px-3 pb-3">
|
||||
<h2 class="mb-2 text-sm font-semibold text-zinc-300">Messungen</h2>
|
||||
{#each protocolMeasurements() as m (m.clientId)}
|
||||
<div class="mb-1.5 rounded-lg bg-zinc-900 p-2.5">
|
||||
<div
|
||||
id="mess-{m.clientId}"
|
||||
class="mb-1.5 rounded-lg bg-zinc-900 p-2.5 transition-shadow {letzteMessungId === m.clientId
|
||||
? 'ring-1 ring-sky-600'
|
||||
: ''}"
|
||||
>
|
||||
<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>
|
||||
|
|
@ -476,6 +625,12 @@
|
|||
<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>
|
||||
<!-- Verwendete Parameter: seit dem Schnellstart laeuft eine Messung
|
||||
auch ohne Dialog, dann ist das hier der einzige Ort, an dem man
|
||||
sieht, WOMIT gemessen wurde. -->
|
||||
{#if parameterZeile(m)}
|
||||
<p class="mt-0.5 text-[11px] text-zinc-500">{parameterZeile(m)}</p>
|
||||
{/if}
|
||||
<div class="mt-0.5"><MeasurementResult result={m.result} /></div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -500,6 +655,10 @@
|
|||
</p>
|
||||
{/if}
|
||||
{#each sortedDevices as device (device.clientId)}
|
||||
<!-- Anker fuer den Sprung nach einer Geraete-Messung: das Ergebnis
|
||||
wird INNERHALB der Geraetekarte gerendert, nicht in der
|
||||
Messungen-Liste. -->
|
||||
<div id="ger-{device.clientId}">
|
||||
<DeviceCard
|
||||
{device}
|
||||
measurements={measurementsFor(device.clientId)}
|
||||
|
|
@ -508,6 +667,7 @@
|
|||
onfavorite={() => doToggleFav(device)}
|
||||
onrename={() => (renameTarget = device)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
|
|
@ -574,13 +734,20 @@
|
|||
</div>
|
||||
|
||||
{#if activeTool}
|
||||
<!-- {#key} ist noetig: ToolDialog belegt seine Parameter genau einmal beim
|
||||
Aufbau. Ohne den Schluessel behielte ein bereits gemounteter Dialog die
|
||||
Werte des vorigen Werkzeugs. -->
|
||||
{#key activeTool.id}
|
||||
<ToolDialog
|
||||
tool={activeTool}
|
||||
{protocol}
|
||||
device={activeDevice}
|
||||
autostart={sofortStarten}
|
||||
{startParams}
|
||||
onclose={() => (activeTool = null)}
|
||||
onrun={runTool}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
{#if renameTarget}
|
||||
|
|
|
|||
Loading…
Reference in a new issue