Compare commits
No commits in common. "295ccb2e62a49858d8961890abcfef2108c20c65" and "c98f2b11522f9fdab2cea459d25ac4107189105f" have entirely different histories.
295ccb2e62
...
c98f2b1152
10 changed files with 21 additions and 416 deletions
|
|
@ -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`). */
|
/** Öffnet die System-Ortungseinstellungen (für den Klartext-Hinweis bei `isLocationEnabled()==false`). */
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun openLocationSettings(call: PluginCall) {
|
fun openLocationSettings(call: PluginCall) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
*/
|
*/
|
||||||
import { Star, Pencil } from 'lucide-svelte';
|
import { Star, Pencil } from 'lucide-svelte';
|
||||||
import MeasurementResult from './MeasurementResult.svelte';
|
import MeasurementResult from './MeasurementResult.svelte';
|
||||||
import AmpelBadge from './AmpelBadge.svelte';
|
|
||||||
import type { Device, Measurement } from '$lib/types';
|
import type { Device, Measurement } from '$lib/types';
|
||||||
import type { Tool } from '$lib/tools/types';
|
import type { Tool } from '$lib/tools/types';
|
||||||
|
|
||||||
|
|
@ -123,7 +122,7 @@
|
||||||
|
|
||||||
{#each measurements as m (m.clientId)}
|
{#each measurements as m (m.clientId)}
|
||||||
<div class="mt-1.5 flex items-start gap-2 border-t border-zinc-800 pt-1.5">
|
<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">
|
<div class="min-w-0">
|
||||||
<p class="text-xs {ampel[m.measureStatus]}">{m.label}</p>
|
<p class="text-xs {ampel[m.measureStatus]}">{m.label}</p>
|
||||||
<MeasurementResult result={m.result} />
|
<MeasurementResult result={m.result} />
|
||||||
|
|
|
||||||
|
|
@ -2,73 +2,44 @@
|
||||||
/**
|
/**
|
||||||
* Stellt das `result`-Objekt einer Messung lesbar dar.
|
* Stellt das `result`-Objekt einer Messung lesbar dar.
|
||||||
*
|
*
|
||||||
* Drei Fälle, weil die Werkzeuge alle drei liefern:
|
* Skalare Werte (Latenz, Anzahl, …) kommen kompakt in eine Zeile. Arrays
|
||||||
* - Skalare (Latenz, Anzahl) → kompakte Zeile mit Klartext und Einheit
|
* — WLAN-Netze, Traceroute-Hops — werden als echte, zeilenweise Liste
|
||||||
* - Arrays → zeilenweise Liste, nicht in eine Komma-Wurst gequetscht
|
* dargestellt statt in eine endlose Komma-Zeile 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();
|
let { result }: { result: Record<string, unknown> } = $props();
|
||||||
|
|
||||||
|
/** Leere Werte ('', null, undefined, leeres Array) ausblenden */
|
||||||
function isEmpty(v: unknown): boolean {
|
function isEmpty(v: unknown): boolean {
|
||||||
if (v == null || v === '') return true;
|
if (v == null || v === '') return true;
|
||||||
if (Array.isArray(v)) return v.length === 0;
|
if (Array.isArray(v)) return v.length === 0;
|
||||||
if (typeof v === 'object') return Object.keys(v as object).length === 0;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = $derived(Object.entries(result).filter(([, v]) => !isEmpty(v)));
|
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 „schlüssel: wert · schlüssel: wert"-Zeile */
|
||||||
|
|
||||||
/** Skalare als eine kompakte Zeile */
|
|
||||||
const scalarText = $derived(
|
const scalarText = $derived(
|
||||||
entries
|
entries
|
||||||
.filter(([, v]) => !Array.isArray(v) && !istObjekt(v))
|
.filter(([, v]) => !Array.isArray(v))
|
||||||
.map(([k, v]) => `${feldLabel(k)}: ${feldWert(k, v)}`)
|
.map(([k, v]) => `${k}: ${String(v)}`)
|
||||||
.join(' · '),
|
.join(' · '),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Einzelne Objekte (z.B. „eigenesNetz") als eine verdichtete Zeile */
|
/** Array-Werte je als eigene Liste */
|
||||||
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[]][]);
|
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>
|
</script>
|
||||||
|
|
||||||
{#if scalarText}
|
{#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}
|
{/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)}
|
{#each lists as [key, items] (key)}
|
||||||
<div class="mt-0.5">
|
<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">
|
<ul class="mt-0.5 flex flex-col gap-0.5">
|
||||||
{#each items as item, i (i)}
|
{#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}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,5 @@
|
||||||
<script lang="ts">
|
<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 { 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 = {
|
const colors = {
|
||||||
info: 'bg-zinc-700',
|
info: 'bg-zinc-700',
|
||||||
|
|
@ -33,21 +8,10 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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">
|
||||||
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)}
|
{#each toast.items as item (item.id)}
|
||||||
<button
|
<div class="rounded-lg px-4 py-2 text-sm text-white shadow-lg {colors[item.type]}">
|
||||||
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.text}
|
||||||
item.type
|
</div>
|
||||||
]}"
|
|
||||||
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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import type { Tool, ToolPreview } from '$lib/tools/types';
|
import type { Tool, ToolPreview } from '$lib/tools/types';
|
||||||
import type { Device, Protocol } from '$lib/types';
|
import type { Device, Protocol } from '$lib/types';
|
||||||
import { debugLog } from '$lib/debuglog.svelte';
|
import { debugLog } from '$lib/debuglog.svelte';
|
||||||
import { scanner } from '$lib/scanner';
|
|
||||||
import { X } from 'lucide-svelte';
|
import { X } from 'lucide-svelte';
|
||||||
|
|
||||||
type LiveProgress = { done: number; total: number; found: number; foundIps?: string[] };
|
type LiveProgress = { done: number; total: number; found: number; foundIps?: string[] };
|
||||||
|
|
@ -79,9 +78,6 @@
|
||||||
progress = tool.supportsProgress ? { done: 0, total: 0, found: 0 } : null;
|
progress = tool.supportsProgress ? { done: 0, total: 0, found: 0 } : null;
|
||||||
if (tool.supportsProgress) step = 'running';
|
if (tool.supportsProgress) step = 'running';
|
||||||
busy = true;
|
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 {
|
try {
|
||||||
await onrun(
|
await onrun(
|
||||||
{ ...params },
|
{ ...params },
|
||||||
|
|
@ -103,7 +99,6 @@
|
||||||
step = 'params';
|
step = 'params';
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
void scanner.setKeepAwake({ on: false }).catch(() => {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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) */
|
/** Öffnet die System-Ortungseinstellungen — für den Hinweis, wenn WLAN-Scan wegen ausgeschalteter Ortung leer bleibt (vor Android 13) */
|
||||||
openLocationSettings(): Promise<{ ok: boolean }>;
|
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) */
|
/** DHCP-Lease-Info des Geräts (Server, Lease-Dauer, Gateway, DNS) */
|
||||||
dhcpInfo(): Promise<DhcpLease>;
|
dhcpInfo(): Promise<DhcpLease>;
|
||||||
/** SNMP v2c Abfrage (Switch: Link-Speed, Fehlerzähler) */
|
/** SNMP v2c Abfrage (Switch: Link-Speed, Fehlerzähler) */
|
||||||
|
|
@ -650,9 +644,6 @@ const mock: NetDiagScannerPlugin = {
|
||||||
async openLocationSettings() {
|
async openLocationSettings() {
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
},
|
},
|
||||||
async setKeepAwake(opts) {
|
|
||||||
return { on: opts.on };
|
|
||||||
},
|
|
||||||
async dhcpInfo() {
|
async dhcpInfo() {
|
||||||
return {
|
return {
|
||||||
server: '192.168.1.1',
|
server: '192.168.1.1',
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,7 @@ class ToastState {
|
||||||
private lastKey = '';
|
private lastKey = '';
|
||||||
private lastAt = 0;
|
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 key = `${type}:${text}`;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// Gleiche Meldung binnen 2 s nicht erneut zeigen
|
// Gleiche Meldung binnen 2 s nicht erneut zeigen
|
||||||
|
|
@ -38,12 +29,7 @@ class ToastState {
|
||||||
this.items = [...this.items, { id, text, type }];
|
this.items = [...this.items, { id, text, type }];
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.items = this.items.filter((t) => t.id !== id);
|
this.items = this.items.filter((t) => t.id !== id);
|
||||||
}, dauer);
|
}, durationMs);
|
||||||
}
|
|
||||||
|
|
||||||
/** Meldung von Hand schließen (Antippen) */
|
|
||||||
dismiss(id: number): void {
|
|
||||||
this.items = this.items.filter((t) => t.id !== id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
import AppHeader from '$lib/components/AppHeader.svelte';
|
import AppHeader from '$lib/components/AppHeader.svelte';
|
||||||
import ToolDialog from '$lib/components/ToolDialog.svelte';
|
import ToolDialog from '$lib/components/ToolDialog.svelte';
|
||||||
import MeasurementResult from '$lib/components/MeasurementResult.svelte';
|
import MeasurementResult from '$lib/components/MeasurementResult.svelte';
|
||||||
import AmpelBadge from '$lib/components/AmpelBadge.svelte';
|
|
||||||
import DeviceCard from '$lib/components/DeviceCard.svelte';
|
import DeviceCard from '$lib/components/DeviceCard.svelte';
|
||||||
import TextPromptDialog from '$lib/components/TextPromptDialog.svelte';
|
import TextPromptDialog from '$lib/components/TextPromptDialog.svelte';
|
||||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||||
|
|
@ -25,7 +24,6 @@
|
||||||
import { toast } from '$lib/toast.svelte';
|
import { toast } from '$lib/toast.svelte';
|
||||||
import { pushOverlay } from '$lib/overlay.svelte';
|
import { pushOverlay } from '$lib/overlay.svelte';
|
||||||
import { TOOLS, getTool } from '$lib/tools';
|
import { TOOLS, getTool } from '$lib/tools';
|
||||||
import { werkzeugName } from '$lib/messfelder';
|
|
||||||
import type { Tool } from '$lib/tools/types';
|
import type { Tool } from '$lib/tools/types';
|
||||||
import type { Device, Protocol } from '$lib/types';
|
import type { Device, Protocol } from '$lib/types';
|
||||||
import * as Icons from 'lucide-svelte';
|
import * as Icons from 'lucide-svelte';
|
||||||
|
|
@ -48,18 +46,6 @@
|
||||||
const protocolTools = TOOLS.filter((t) => t.scope === 'protocol' && t.id !== 'stresstest');
|
const protocolTools = TOOLS.filter((t) => t.scope === 'protocol' && t.id !== 'stresstest');
|
||||||
const deviceTools = TOOLS.filter((t) => t.scope === 'device');
|
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") */
|
/** IP-Adressen numerisch vergleichen (nicht als Text — "…9" vor "…10") */
|
||||||
function ipCompare(a: string, b: string): number {
|
function ipCompare(a: string, b: string): number {
|
||||||
const pa = a.split('.').map(Number);
|
const pa = a.split('.').map(Number);
|
||||||
|
|
@ -395,11 +381,8 @@
|
||||||
{#each protocolMeasurements() as m (m.clientId)}
|
{#each protocolMeasurements() as m (m.clientId)}
|
||||||
<div class="mb-1.5 rounded-lg bg-zinc-900 p-2.5">
|
<div class="mb-1.5 rounded-lg bg-zinc-900 p-2.5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<AmpelBadge status={m.measureStatus} />
|
<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 ?? werkzeugName(m.tool)}</span>
|
<span class="text-sm font-medium">{getTool(m.tool)?.name ?? 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>
|
</div>
|
||||||
<p class="mt-1 text-xs {ampel[m.measureStatus]}">{m.label}</p>
|
<p class="mt-1 text-xs {ampel[m.measureStatus]}">{m.label}</p>
|
||||||
<div class="mt-0.5"><MeasurementResult result={m.result} /></div>
|
<div class="mt-0.5"><MeasurementResult result={m.result} /></div>
|
||||||
|
|
@ -484,12 +467,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Abschluss-Leiste -->
|
<!-- Abschluss-Leiste -->
|
||||||
<!-- Feste Aktionsleiste. Sie setzt --leiste-hoehe, damit die Kurzmeldungen
|
<div class="fixed inset-x-0 bottom-0 border-t border-zinc-800 bg-zinc-900 p-3 safe-bottom">
|
||||||
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
|
<button
|
||||||
class="w-full rounded-lg bg-emerald-600 py-2.5 font-semibold text-white active:bg-emerald-700 disabled:opacity-50"
|
class="w-full rounded-lg bg-emerald-600 py-2.5 font-semibold text-white active:bg-emerald-700 disabled:opacity-50"
|
||||||
onclick={finish}
|
onclick={finish}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue