Modbus/LPD in der Schnellprobe, Dosen-Bewertung, Loeschen ins Ueberlaufmenue

Port-Schnellprobe um 502 (Modbus) und 515 (LPD) erweitert. 502 ist in diesem
Betrieb der wichtigste Hinweis ueberhaupt - Wechselrichter, Batteriespeicher
und Wallboxen sprechen Modbus/TCP. Die Geraeteart-Heuristik wertet 502 laengst
aus ("IoT/SPS"), der Zweig war nur nachweislich unerreichbar, weil die Sonde
den Port gar nicht geprueft hat. Dasselbe bei 515 fuer Drucker.
Laufzeit vorher/nachher im Emulator gemessen (voller /24): 44,8 s -> 52,0 s,
also +16 % bei 20 % mehr Ports. Proportional und vertretbar.
88 und 8443 bewusst NICHT in die Schnellprobe: sie kommen im Handwerksnetz
praktisch nicht vor, und dieser Pfad laeuft je stiller Adresse in beiden
Runden plus in der Anreicherung. Im Port-Scan auf ein einzelnes Geraet sind
sie dagegen jetzt dabei, ebenso 554 (RTSP) und 9100.
Servicenamen ergaenzt, damit im Protokoll nicht die nackte Portnummer steht.

IP-Test bewertet jetzt die Dose statt nur "IP ja/nein". Das ist der
Elektro-Abnahmebeleg schlechthin: 100 Mbit auf einer Cat-7-Leitung heisst so
gut wie immer, dass zwei Aderpaare nicht aufgelegt sind oder die Dose falsch
geklemmt ist - ein Befund, den der Techniker sofort beheben kann, solange er
noch auf der Leiter steht. Vorher stand dort eine gruene Ampel, weil ja eine
IP da war. Unbekannte Geschwindigkeit ergibt Status 3 statt 0: bei manchen
Android-Versionen ist /sys/class/net/<iface>/speed gesperrt, und eine Abnahme
darf nicht auf einer Angabe beruhen, die es nicht gibt.

"Protokoll loeschen" ist ins Ueberlaufmenue der Kopfzeile gewandert. Es stand
als roter Link unmittelbar ueber dem gruenen "Abschliessen & synchronisieren" -
bei offener Tastatur liegen beide im selben Daumenbereich, und ein Fehlgriff
loescht ein noch nicht synchronisiertes Protokoll unwiederbringlich.
AppHeader hat dafuer ein optionales Snippet bekommen (die Komponente wird auf
zehn Seiten benutzt, das Zahnrad darf durch nichts verdraengt werden). Das
Menue meldet sich als Overlay an, sonst haette der Hardware-Zurueck das
Protokoll verlassen statt das Menue zu schliessen.

Im Emulator geprueft.
This commit is contained in:
Eduard Wisch 2026-08-16 19:27:33 +02:00
parent 6483589592
commit 0fe938a764
5 changed files with 152 additions and 14 deletions

View file

@ -499,7 +499,21 @@ class NetDiagScannerPlugin : Plugin() {
* Geräteart-Heuristik (z.B. 554 Kamera, 9100 Drucker). 500 ms Timeout. * Geräteart-Heuristik (z.B. 554 Kamera, 9100 Drucker). 500 ms Timeout.
*/ */
private suspend fun quickPortProbe(ip: String): List<Int> { private suspend fun quickPortProbe(ip: String): List<Int> {
val probe = listOf(22, 23, 80, 443, 445, 554, 1883, 3389, 8000, 9100) /*
* 502 (Modbus) und 515 (LPD) ergaenzt.
*
* 502 ist in diesem Betrieb der wichtigste Hinweis ueberhaupt:
* Wechselrichter, Batteriespeicher und Wallboxen sprechen Modbus/TCP.
* Die Geraeteart-Heuristik weiter unten wertet 502 laengst aus ("IoT/SPS")
* der Zweig war nur nachweislich unerreichbar, weil die Sonde den Port
* gar nicht geprueft hat. Dasselbe gilt fuer 515 (Drucker).
*
* 88 (Kerberos) und 8443 bewusst NICHT hier: sie kommen im Handwerksnetz
* praktisch nicht vor und dieser Pfad ist der teuerste im ganzen Scan
* (er laeuft je stiller Adresse in BEIDEN Runden plus in der
* Anreicherung). Wer sie braucht, nimmt den Port-Scan mit eigener Liste.
*/
val probe = listOf(22, 23, 80, 443, 445, 502, 515, 554, 1883, 3389, 8000, 9100)
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
probe.map { port -> probe.map { port ->
async { async {
@ -2461,8 +2475,10 @@ class NetDiagScannerPlugin : Plugin() {
private fun serviceName(port: Int): String = when (port) { private fun serviceName(port: Int): String = when (port) {
21 -> "ftp"; 22 -> "ssh"; 23 -> "telnet"; 53 -> "dns"; 80 -> "http" 21 -> "ftp"; 22 -> "ssh"; 23 -> "telnet"; 53 -> "dns"; 80 -> "http"
139 -> "netbios"; 443 -> "https"; 445 -> "smb"; 502 -> "modbus" 88 -> "kerberos"; 139 -> "netbios"; 443 -> "https"; 445 -> "smb"
1883 -> "mqtt"; 3389 -> "rdp"; 8080 -> "http-alt"; 8443 -> "https-alt" 502 -> "modbus"; 515 -> "lpd"; 554 -> "rtsp"; 1883 -> "mqtt"
3389 -> "rdp"; 8000 -> "http-alt"; 8080 -> "http-alt"
8443 -> "https-alt"; 9100 -> "jetdirect"
else -> "" else -> ""
} }

View file

@ -6,7 +6,18 @@
title, title,
back = false, back = false,
subtitle = '', subtitle = '',
}: { title: string; back?: boolean; subtitle?: string } = $props(); aktionen = undefined,
}: {
title: string;
back?: boolean;
subtitle?: string;
/**
* Zusätzliche Bedienelemente links vom Zahnrad (z.B. ein Überlaufmenü).
* Optional — die Kopfzeile wird auf zehn Seiten benutzt, und das Zahnrad
* darf durch nichts verdrängt werden.
*/
aktionen?: import('svelte').Snippet;
} = $props();
// Zahnrad auf der Einstellungs-Seite selbst ausblenden // Zahnrad auf der Einstellungs-Seite selbst ausblenden
const onSettings = $derived($page.url.pathname.startsWith('/einstellungen')); const onSettings = $derived($page.url.pathname.startsWith('/einstellungen'));
@ -24,6 +35,7 @@
<p class="truncate text-xs text-zinc-400">{subtitle}</p> <p class="truncate text-xs text-zinc-400">{subtitle}</p>
{/if} {/if}
</div> </div>
{@render aktionen?.()}
{#if !onSettings} {#if !onSettings}
<a <a
class="rounded p-1.5 text-zinc-300 active:bg-zinc-800" class="rounded p-1.5 text-zinc-300 active:bg-zinc-800"

View file

@ -6,8 +6,17 @@
import { scanner } from '../../scanner'; import { scanner } from '../../scanner';
import type { Tool } from '../types'; import type { Tool } from '../types';
/** Häufige Ports im Standard-Scan */ /**
const DEFAULT_PORTS = [21, 22, 23, 53, 80, 139, 443, 445, 502, 1883, 3389, 8080, 8443]; * Häufige Ports im Standard-Scan.
*
* Hier darf die Liste länger sein als bei der Schnellprobe des IP-Scanners:
* der Port-Scan läuft auf EIN vom Techniker ausgewähltes Gerät, nicht auf 254
* Adressen in zwei Runden. Ergänzt gegenüber der Schnellprobe: 515 (Drucker
* per LPD), 554 (Kamera per RTSP), 9100 (Drucker roh), 88 (Windows-Domäne).
*/
const DEFAULT_PORTS = [
21, 22, 23, 53, 80, 88, 139, 443, 445, 502, 515, 554, 1883, 3389, 8080, 8443, 9100,
];
export const portScanTool: Tool = { export const portScanTool: Tool = {
id: 'portscan', id: 'portscan',

View file

@ -169,6 +169,13 @@
return pushOverlay(() => (activeTool = null)); return pushOverlay(() => (activeTool = null));
}); });
// Dasselbe fürs Überlaufmenü — ohne diese Anmeldung würde der Zurück-Knopf
// das Protokoll verlassen, während das Menü noch offen ist.
$effect(() => {
if (!ueberlaufOffen) return;
return pushOverlay(() => (ueberlaufOffen = false));
});
/** Lucide-Icon dynamisch holen (Tool-Icon-Name ist kebab-case) */ /** Lucide-Icon dynamisch holen (Tool-Icon-Name ist kebab-case) */
function icon(name: string) { function icon(name: string) {
const pascal = name const pascal = name
@ -440,6 +447,7 @@
title={protocol.label} title={protocol.label}
subtitle={protocol.socName || protocol.orderRef || 'Diagnose'} subtitle={protocol.socName || protocol.orderRef || 'Diagnose'}
back back
aktionen={ueberlaufMenue}
/> />
<div class="flex-1 overflow-y-auto pb-24"> <div class="flex-1 overflow-y-auto pb-24">
@ -710,11 +718,6 @@
</section> </section>
{/if} {/if}
<div class="px-3">
<button class="text-xs text-red-400 underline" onclick={() => (confirmDelete = true)}>
Protokoll löschen
</button>
</div>
</div> </div>
<!-- Abschluss-Leiste --> <!-- Abschluss-Leiste -->
@ -761,6 +764,54 @@
/> />
{/if} {/if}
<!--
Ueberlaufmenue. Das Loeschen stand vorher als roter Link unmittelbar ueber
dem gruenen "Abschliessen & synchronisieren" — bei offener Tastatur liegen
beide im selben Daumenbereich, und ein Fehlgriff loescht ein noch nicht
synchronisiertes Protokoll unwiederbringlich. Selten gebrauchte, gefaehrliche
Aktionen gehoeren nicht in den Hauptweg.
-->
{#snippet ueberlaufMenue()}
<button
class="rounded p-1.5 text-zinc-300 active:bg-zinc-800"
onclick={() => (ueberlaufOffen = true)}
aria-label="Weitere Aktionen"
aria-haspopup="menu"
>
<Icons.MoreVertical size={22} />
</button>
{/snippet}
{#if ueberlaufOffen}
<!-- Bewusst ein Blatt von unten statt eines Menues an der Kopfzeile:
einhaendig am Handy ist der obere Bildschirmrand der schlechteste Ort. -->
<div
class="fixed inset-0 z-40 bg-black/60"
role="presentation"
onclick={() => (ueberlaufOffen = false)}
></div>
<div class="fixed inset-x-0 bottom-0 z-50 rounded-t-xl bg-zinc-900 p-3 pb-6" role="menu">
<button
class="flex w-full items-center gap-2 rounded-lg p-3 text-left text-sm text-red-400 active:bg-zinc-800"
role="menuitem"
onclick={() => {
ueberlaufOffen = false;
confirmDelete = true;
}}
>
<Icons.Trash2 size={18} />
Protokoll löschen
</button>
<button
class="mt-1 w-full rounded-lg p-3 text-sm text-zinc-400 active:bg-zinc-800"
role="menuitem"
onclick={() => (ueberlaufOffen = false)}
>
Abbrechen
</button>
</div>
{/if}
{#if confirmDelete} {#if confirmDelete}
<ConfirmDialog <ConfirmDialog
title="Protokoll löschen?" title="Protokoll löschen?"

View file

@ -20,7 +20,7 @@
import { rssiRating } from '$lib/wifi/rating'; import { rssiRating } from '$lib/wifi/rating';
import { sync } from '$lib/sync.svelte'; import { sync } from '$lib/sync.svelte';
import { toast } from '$lib/toast.svelte'; import { toast } from '$lib/toast.svelte';
import type { LinkInfo, Protocol } from '$lib/types'; import type { LinkInfo, MeasureStatus, Protocol } from '$lib/types';
let protocol = $state<Protocol | null>(null); let protocol = $state<Protocol | null>(null);
let links = $state<LinkInfo[]>([]); let links = $state<LinkInfo[]>([]);
@ -104,10 +104,59 @@
saveOpen = true; saveOpen = true;
} }
/**
* Dose bewerten — nicht nur „IP ja/nein".
*
* Das ist der Elektro-Abnahmebeleg schlechthin: 100 Mbit auf einer
* Cat-7-Leitung heißt so gut wie immer, dass zwei Aderpaare nicht aufgelegt
* sind oder die Dose falsch geklemmt ist. Ein Befund, den der Techniker
* sofort beheben kann, solange er noch auf der Leiter steht — vorher stand
* dort eine grüne Ampel, weil ja eine IP da war.
*
* Unbekannte Geschwindigkeit ergibt Status 3 („nicht messbar"), nicht 0:
* bei manchen Android-Versionen ist /sys/class/net/<iface>/speed gesperrt.
* Eine Abnahme darf nicht auf einer Angabe beruhen, die es nicht gibt.
*/
function bewerteDose(l: LinkInfo): { status: MeasureStatus; hinweis?: string } {
if (!l.ipv4) {
return { status: 2, hinweis: 'Keine IP-Adresse — Dose liefert kein Netz.' };
}
const mbit = l.linkSpeedMbps;
if (l.type !== 'ethernet') {
// WLAN: die Aussage über die Dose betrifft nur Kabel
return { status: 0 };
}
if (mbit == null || mbit <= 0) {
return {
status: 3,
hinweis:
'Verbindungsgeschwindigkeit konnte auf diesem Gerät nicht ausgelesen ' +
'werden — über die Leitungsqualität sagt die Messung damit nichts aus.',
};
}
if (mbit < 100) {
return {
status: 2,
hinweis: `Nur ${mbit} Mbit — Leitung, Dose oder Patchfeld prüfen.`,
};
}
if (mbit < 1000) {
return {
status: 1,
hinweis:
`Nur ${mbit} Mbit statt 1000. Typische Ursache: zwei der vier ` +
'Aderpaare sind nicht aufgelegt (Dose oder Patchfeld), seltener eine ' +
'beschädigte Leitung oder ein Gerät, das nur 100 Mbit kann.',
};
}
return { status: 0 };
}
async function doSave(name: string) { async function doSave(name: string) {
saveOpen = false; saveOpen = false;
if (!protocol || !primary) return; if (!protocol || !primary) return;
const raum = name.trim(); const raum = name.trim();
const bewertung = bewerteDose(primary);
addMeasurement(protocol, { addMeasurement(protocol, {
tool: 'iptest', tool: 'iptest',
category: 'netzwerk', category: 'netzwerk',
@ -122,12 +171,13 @@
result: { result: {
raum: raum || undefined, raum: raum || undefined,
...primary, ...primary,
...(bewertung.hinweis ? { hinweis: bewertung.hinweis } : {}),
}, },
measureStatus: primary.ipv4 ? 0 : 2, measureStatus: bewertung.status,
dateMeasure: Date.now(), dateMeasure: Date.now(),
}); });
await persist(); await persist();
toast.show('Messung gespeichert', 'success'); toast.show(bewertung.hinweis ?? 'Messung gespeichert', bewertung.status >= 2 ? 'error' : 'success');
} }
</script> </script>