Kompletter Umbau (ROADMAP_UMSETZUNG.md Phase 2). Die alte Fassung hatte vier Probleme, die den Test praktisch wertlos machten: kein festes Intervall (ein "Messpunkt" dauerte so lange wie das Ziel zufaellig antwortete — Prod #126: 38 Punkte in 300 s statt der ueblichen ~250-300), keine Zeitreihe (nur Summen), kein Foreground-Service trotz gegenteiliger Doku-Behauptung, kein Abbruchpfad. Vor allem aber: waehrend der 1-60 Minuten sah man nichts ausser "Messung laeuft ...". Nativ (NetDiagScannerPlugin.kt, MonitorService.kt) StressRun komplett neu: feste Taktung per naechstem Fixpunkt (nextTick += intervalSec*1000, kein Zeitverzug ueber eine Stunde), Rohzaehler (sentTotal/receivedTotal/rttSum/Min/Max) + echte Zeitreihe (Liste aus {ts,rtt}), Ausfallsegmente werden waehrend des Laufs erkannt und geschlossen. Laeuft im MonitorService (Foreground, wie der Geraete-Monitor) — dafuer wurde der Dienst auf eine Job-Registry umgestellt (jobId -> Text), damit ein gleichzeitig laufender Geraete-Monitor und Dauertest sich beim Beenden nicht gegenseitig die Benachrichtigung wegnehmen. Events stressSample (live, pro Probe) und stressFinished (einmalig, Grund completed/stopped) + getStressStatus zur Wiederaufnahme nach Seitenwechsel. WICHTIG geloest: laeuft der Test natuerlich aus, waehrend niemand zuhoert (App im Hintergrund, andere Seite offen), bleibt der Lauf-Eintrag in stressRuns bestehen (wie monitorRuns) statt sofort geloescht zu werden — sonst waere das Ergebnis unwiederbringlich weg. Neuer Aufraeum-Call dismissStressRun. Nebenbei: der geteilte io-Scope aller Plugin-Methoden lief bisher auf einem gewoehnlichen Job — eine unbehandelte Exception in EINER Coroutine (z.B. IP-Scan mit kaputtem Host) haette strukturell alle Geschwister-Coroutinen mitgerissen, auch einen parallel laufenden Monitor/Dauertest. Jetzt SupervisorJob + CoroutineExceptionHandler. Kritischer Bug gefunden UND gefixt (im Emulator, echter Netzausfall-Test): `.put("rtt", rtt as Any?)` mit Kotlin null wird von org.json.JSONObject behandelt wie `remove(key)` — der Schluessel verschwindet komplett statt als JSON null anzukommen. Die App bekam damit `undefined` statt `null` fuer verlorene Proben; `!== null`-Filter hielten das faelschlich fuer "erhalten". Ergebnis: ein per Flugmodus ausgeloester echter Ausfall zeigte live "0 % Verlust" und "NaN ms" bei Min/Max/Oe. Betraf NUR die Live-Anzeige — die gespeicherte Messung war unabhaengig davon korrekt, weil buildStressResult() ausschliesslich aus dem nativen Kotlin-Zustand rechnet, nie ueber die Pro-Probe-JSON-Bruecke. Fix: ueberall explizit JSONObject.NULL statt bloss `null` (betraf auch die Traceroute-Korrektur aus Phase 1). Zusaetzlich TS-seitig als zweite Verteidigungslinie `!= null` statt `!== null` (deckt sowohl null als auch undefined ab). Neu: gemeinsame Bewertung (tools/rating.ts) ping.ts und der alte Dauertest bewerteten denselben Netzzustand unterschiedlich (ping.ts: avgMs>100 -> Rot; Dauertest: nur lossPct/maxMs). rateLatency() ist jetzt die einzige Stelle: Verlust, Jitter, Latenz nach Zielklasse (LAN/WLAN/WAN) UND eine eigene Regel "jeder Ausfall > 5 s = Rot" unabhaengig vom Gesamt-Verlustprozentsatz. ping.ts umgestellt, unveraendertes Verhalten fuer die bisherigen Schwellen (inkl. Jitter>10ms, das beim Portieren fast uebersehen wurde). Neue Route statt Dialog (routes/protokoll/[id]/stresstest/) Wie IP-Test/WLAN/Monitor eine eigene Seite statt des blockierenden Werkzeug-Dialogs. Einrichtung (Ziel leer=Gateway, Dauer, Takt) mit Vorlaufprobe wie zuvor in stresstest.ts. Laufend: grosse Live-Zahl mit Ampelfarbe (rateLatency), Fortschrittsbalken mit Restzeit, Streifendiagramm (StressChart.svelte, live rollierendes 120s-Fenster), Zaehler gesendet/verloren/Ø/Min/Max, offener-Ausfall-Banner, Ereignisliste mit Uhrzeit ("Ausfall — wieder da nach 16 s"), Abbrechen. Abgeschlossene Laeufe darunter aufklappbar mit demselben Diagramm (kompletter Verlauf statt Fenster). stresstest.ts (Tool-Registry-Eintrag) ist jetzt nur noch Namens-Lookup fuer die Messungen-Liste, run() wirft bewusst einen Fehler, falls doch noch etwas den alten Weg aufruft. Sicherheitsnetz gegen verpasste Events (lib/stresstest.ts) Ein natuerlich beendeter Test, waehrend niemand auf der Stresstest-Seite ist, verpasst dort das stressFinished-Event. Die Hauptprotokollseite prueft deshalb beim Oeffnen zusaetzlich per resumeFinishedStressSessions() nach (praktisch immer besucht, bevor ein Protokoll abgeschlossen wird) und traegt das Ergebnis nach. Bekannter Rest-Fall (dokumentiert): verlaesst der Techniker das Protokoll komplett ohne eine der beiden Seiten noch einmal zu oeffnen, bleibt der fertige Lauf bis zum naechsten Besuch liegen. Ergebnis-Speicherung: Zeitreihe in 1-Minuten-Buckets + Ausfallsegmente + laengster Ausfall als Measurement (tool=stresstest, sync-faehig) — Ausfaelle werden beim Abschliessen mit dem nativen (autoritativen) Ergebnis abgeglichen, nicht nur mit der lokal live mitgefuehrten Kopie. Getestet im Emulator (Pixel 6 / Android 14): - 1-Minuten-Lauf komplett bei ausgeschaltetem Display durchgelaufen (Foreground-Service haelt den Prozess), automatischer Abschluss + Toast - Echter Netzausfall per Flugmodus (15s) waehrend laufendem 5-Minuten-Test: korrekt 23,2%/16,7% Verlust, Min/Max ohne NaN, Ereignisliste mit exakter Uhrzeit und Dauer — nach dem NULL-Fix verifiziert - Abbrechen-Pfad: sofortiger Stopp, korrektes Teil-Ergebnis - Wiederaufnahme: laufender Test nach APK-Neuinstallation (Prozess-Kill) korrekt als beendet erkannt - gradle compileDebugKotlin ok, svelte-check 0 Fehler Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
107 lines
3.9 KiB
Svelte
107 lines
3.9 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* Streifendiagramm eines Dauer-/Stresstests — Laufzeit über die Zeit, mit
|
|
* Ausfällen als rote Bänder statt als glatte Linie durch die Lücke. Wird
|
|
* sowohl für den laufenden Test (windowSec gesetzt = rollierendes Fenster)
|
|
* als auch für einen abgeschlossenen Lauf (windowSec leer = ganzer Verlauf)
|
|
* verwendet.
|
|
*
|
|
* Bewusst OHNE Achsenbeschriftung im SVG: `preserveAspectRatio="none"` zieht
|
|
* die Zeichenfläche auf die Container-Breite — bei eingebettetem `<text>`
|
|
* würde das die Schrift verzerren. Kennzahlen (Max, Ø, …) zeigt die
|
|
* aufrufende Seite als eigene Kacheln daneben.
|
|
*/
|
|
import type { StressOutage, StressSample } from '$lib/types';
|
|
|
|
let {
|
|
samples,
|
|
outages,
|
|
intervalSec = 1,
|
|
windowSec = undefined,
|
|
}: {
|
|
samples: StressSample[];
|
|
outages: StressOutage[];
|
|
intervalSec?: number;
|
|
/** rollierendes Fenster in Sekunden; weglassen = kompletter Verlauf */
|
|
windowSec?: number;
|
|
} = $props();
|
|
|
|
const W = 300;
|
|
const H = 90;
|
|
|
|
const view = $derived.by(() => {
|
|
const now = Date.now();
|
|
const cutoff = windowSec ? now - windowSec * 1000 : -Infinity;
|
|
const tail = samples.filter((s) => s.ts >= cutoff);
|
|
if (tail.length === 0) return null;
|
|
|
|
const t0 = windowSec ? cutoff : tail[0].ts;
|
|
const t1 = windowSec ? now : tail[tail.length - 1].ts;
|
|
const spanMs = Math.max(1000, t1 - t0);
|
|
|
|
const receivedVals = tail.map((s) => s.rtt).filter((v): v is number => v != null);
|
|
const maxRtt = Math.max(20, ...receivedVals, 0);
|
|
|
|
const x = (ts: number) => ((ts - t0) / spanMs) * W;
|
|
const y = (rtt: number) => H - (rtt / maxRtt) * H;
|
|
|
|
// In Segmente teilen: eine Lücke (verlorene Probe oder größere Zeit-
|
|
// Unterbrechung) darf NICHT als gerade Linie überbrückt werden — das
|
|
// würde eine Messung vortäuschen, die es nicht gab.
|
|
const gapMs = Math.max(intervalSec, 1) * 1500;
|
|
const segments: string[] = [];
|
|
let current: string[] = [];
|
|
let prevTs: number | null = null;
|
|
for (const s of tail) {
|
|
if (s.rtt == null) {
|
|
if (current.length > 1) segments.push(current.join(' '));
|
|
current = [];
|
|
prevTs = null;
|
|
continue;
|
|
}
|
|
if (prevTs !== null && s.ts - prevTs > gapMs) {
|
|
if (current.length > 1) segments.push(current.join(' '));
|
|
current = [];
|
|
}
|
|
current.push(`${x(s.ts).toFixed(1)},${y(s.rtt).toFixed(1)}`);
|
|
prevTs = s.ts;
|
|
}
|
|
if (current.length > 1) segments.push(current.join(' '));
|
|
|
|
// Ausfallbänder — laufender Ausfall (kein endTs) reicht bis "jetzt"
|
|
const bands = outages
|
|
.filter((o) => (o.endTs ?? now) >= t0)
|
|
.map((o) => {
|
|
const xs = x(Math.max(o.startTs, t0));
|
|
const xe = x(Math.min(o.endTs ?? now, t1));
|
|
return { x: xs, w: Math.max(1.5, xe - xs) };
|
|
});
|
|
|
|
// Letzter Punkt hervorgehoben, wenn er zum sichtbaren Fenster gehört
|
|
const last = tail[tail.length - 1];
|
|
const lastPoint = last && last.rtt != null ? { x: x(last.ts), y: y(last.rtt) } : null;
|
|
|
|
return { segments, bands, lastPoint, maxRtt };
|
|
});
|
|
</script>
|
|
|
|
{#if view}
|
|
<svg viewBox="0 0 {W} {H}" class="w-full" preserveAspectRatio="none" role="img" aria-label="Verlauf der Laufzeit">
|
|
<!-- Nulllinie -->
|
|
<line x1="0" y1={H - 0.5} x2={W} y2={H - 0.5} stroke="currentColor" class="text-zinc-800" stroke-width="1" />
|
|
|
|
{#each view.bands as b (b.x)}
|
|
<rect x={b.x} y="0" width={b.w} height={H} class="fill-red-500/20" />
|
|
{/each}
|
|
|
|
{#each view.segments as seg, i (i)}
|
|
<polyline points={seg} fill="none" stroke="currentColor" stroke-width="2" class="text-sky-400" />
|
|
{/each}
|
|
|
|
{#if view.lastPoint}
|
|
<circle cx={view.lastPoint.x} cy={view.lastPoint.y} r="2.5" class="fill-sky-300" />
|
|
{/if}
|
|
</svg>
|
|
{:else}
|
|
<div class="flex h-[90px] items-center justify-center text-xs text-zinc-600">Noch keine Daten</div>
|
|
{/if}
|