Compare commits

..

No commits in common. "701192b53c975777d3399ca2675420afa0c9648d" and "7236017e87ec47facf0bcb0d74571fe2fbefe34f" have entirely different histories.

9 changed files with 7 additions and 688 deletions

View file

@ -155,62 +155,13 @@ export function searchCustomers(q: string): Promise<{ customers: Customer[] }> {
return request<{ customers: Customer[] }>(`customers.php?q=${encodeURIComponent(q)}`);
}
/** Kurzform eines Protokolls, wie sie in der Kundenübersicht steht */
export interface ProtocolListItem {
id: number;
ref?: string;
label?: string;
clientUuid?: string;
socId?: number | null;
orderId?: number | null;
/** Unix-Sekunden (nicht Millisekunden — so liefert es die API) */
dateDiag?: number;
location?: string;
subnet?: string;
status?: number;
deviceCount?: number;
/** heißt serverseitig `measureCount` — nicht `measurementCount` (netdiag_api.lib.php) */
measureCount?: number;
}
/** Einzelnen Kunden mit Aufträgen und Protokollen laden */
export function getCustomer(
id: number,
): Promise<{ customer: Customer; orders: Order[]; protocols: ProtocolListItem[] }> {
): Promise<{ customer: Customer; orders: Order[]; protocols: unknown[] }> {
return request(`customers.php?id=${id}`);
}
/** Ein Gerät, wie es der Server im Protokoll liefert */
export interface ServerDevice {
id: number;
ip: string;
mac?: string | null;
hostname?: string | null;
netbiosName?: string | null;
mdnsName?: string | null;
mdnsServices?: string[];
customName?: string | null;
vendor?: string | null;
deviceType?: string | null;
openPorts?: number[];
foundVia?: string | null;
lastSeen?: number | null;
note?: string | null;
}
/**
* Ein Protokoll vom Server laden samt Geräten und Messungen.
*
* Gebraucht vom Geräte-Vergleich, der die Liste des letzten Besuchs braucht.
* Bewusst NICHT in den lokalen Bestand übernommen: das frühere Protokoll wird
* nur gelesen, nie überschrieben.
*/
export function getProtocol(
id: number,
): Promise<{ protocol: Record<string, unknown>; devices: ServerDevice[]; measurements: unknown[] }> {
return request(`protocols.php?id=${id}`);
}
export interface SyncResult {
ok: boolean;
protocolId: number;

View file

@ -65,13 +65,6 @@ const FELDER: Record<string, Feld> = {
// DNS-Prüfung (je ein Paket an je einen Server)
gefragteServer: { label: 'Befragte DNS-Server' },
antworten: { label: 'Antworten der Server' },
// Geräte-Vergleich zweier Protokolle
verglichenMit: { label: 'Verglichen mit' },
geraeteVorher: { label: 'Geräte beim letzten Besuch' },
geraeteJetzt: { label: 'Geräte jetzt' },
mitNeuerAdresse: { label: 'Mit neuer IP-Adresse' },
andereOffenePorts: { label: 'Gleiche Adresse, andere offene Ports' },
unveraendert: { label: 'Unverändert' },
// Netz / DHCP / Dose
server: { label: 'DHCP-Server' },
dhcpServer: { label: 'DHCP-Server' },

View file

@ -1,74 +0,0 @@
/**
* Prüfung der Server-Adresse ohne Laufzeit-Import, damit
* `tools/pruefe-serveradresse.mjs` sie nachrechnen kann.
*
* Anlass (16./17.08.2026, im Emulator gefunden): Wird hier eine `http://`-Adresse
* eingetragen, funktioniert die App **stumm nicht**. Sie meldet nur Keine
* Verbindung zum Server"; der wahre Grund steht ausschließlich im `logcat`, an
* den beim Kunden niemand kommt. Es sind sogar zwei Sperren übereinander:
*
* 1. **Mixed Content** die App läuft auf der Origin `https://localhost`; der
* WebView blockiert daraus jede `http://`-Anfrage.
* 2. **Androids Cleartext-Richtlinie** ab API 28 ist unverschlüsseltes HTTP
* ohne `android:usesCleartextTraffic` grundsätzlich verboten.
*
* Deshalb wird die Adresse hier abgewiesen, statt sie anzunehmen und den
* Techniker vor Ort raten zu lassen.
*
* WICHTIG warum nicht einfach http verbieten": Es hängt an der Origin der
* App, nicht an einer festen Regel. Ein Debug-Build mit
* `server.androidScheme: 'http'` (der Weg, auf dem die lokale Testinstanz
* überhaupt prüfbar ist, siehe KB #1089) läuft selbst auf `http://localhost`
* und darf HTTP-Server ansprechen. Die Prüfung fragt deshalb die eigene
* Herkunft ab sonst wäre ausgerechnet der Testbau unbenutzbar.
*/
export interface AdressPruefung {
ok: boolean;
/** Meldung für den Benutzer; leer wenn alles in Ordnung ist */
meldung: string;
}
/**
* Server-Adresse prüfen.
*
* @param eingabe Rohe Eingabe aus dem Feld
* @param appProtokoll Protokoll der App-Origin `window.location.protocol`,
* also 'https:' oder 'http:'
*/
export function pruefeServerAdresse(eingabe: string, appProtokoll: string): AdressPruefung {
const url = (eingabe ?? '').trim();
if (!url) {
return { ok: false, meldung: 'Server-Adresse fehlt' };
}
if (!/^https?:\/\//i.test(url)) {
return { ok: false, meldung: 'Adresse muss mit http:// oder https:// beginnen' };
}
// Tippfehler wie "https://" ohne Host früh abfangen — sonst schlägt erst der
// erste Aufruf fehl, und zwar mit derselben nichtssagenden Meldung.
let host = '';
try {
host = new URL(url).hostname;
} catch {
return { ok: false, meldung: 'Adresse ist keine gültige URL' };
}
if (!host) {
return { ok: false, meldung: 'In der Adresse fehlt der Servername' };
}
const appLaeuftUeberHttps = appProtokoll.toLowerCase().startsWith('https');
const zielIstHttp = /^http:\/\//i.test(url);
if (zielIstHttp && appLaeuftUeberHttps) {
return {
ok: false,
meldung:
'Diese App kann nur HTTPS-Server erreichen — Android blockiert unverschlüsselte ' +
'Verbindungen aus der App heraus. Mit einer http://-Adresse käme nur „Keine ' +
'Verbindung zum Server", ohne erkennbaren Grund.',
};
}
return { ok: true, meldung: '' };
}

View file

@ -10,7 +10,6 @@
import type { Tool, ToolCategory } from './types';
import { dnsCheckTool } from './netzwerk/dnscheck';
import { geraeteDiffTool } from './netzwerk/geraetediff';
import { ipConflictTool } from './netzwerk/ipconflict';
import { ipScanTool } from './netzwerk/ipscan';
import { pingTool } from './netzwerk/ping';
@ -39,7 +38,6 @@ export const TOOLS: Tool[] = [
snmpTool,
tracerouteTool,
dnsCheckTool,
geraeteDiffTool,
stressTestTool,
// Internet — die Kette steht bewusst vorn: sie ist die Erstdiagnose,
// wenn der Kunde „Internet geht nicht" sagt.

View file

@ -1,266 +0,0 @@
/**
* Geräte-Vergleich zweier Protokolle ohne Laufzeit-Import, damit
* `tools/pruefe-geraetediff.mjs` die Zuordnung nachrechnen kann.
*
* WARUM NICHT ÜBER DIE MAC-ADRESSE
*
* Der naheliegende Weg wäre, Geräte über ihre MAC wiederzuerkennen. Er
* funktioniert hier nachweislich nicht: Im ausgelieferten Protokoll ND2026-0015
* sind bei **allen 68 Geräten** MAC, Hostname und Hersteller leer, weil
* `/proc/net/arp` ab Android 10 gesperrt ist. Ein MAC-Diff hätte dort bei jedem
* Besuch alles neu, alles verschwunden" gemeldet eine Aussage, die schlimmer
* ist als gar keine, weil sie im Kundendokument nach einem umgebauten Netz
* aussieht.
*
* ACHTUNG BEIM PRÜFEN: Der Browser-Mock gibt jedem Gerät eine MAC und meldet
* `arpAvailable: true`. Ein MAC-Diff sieht dort perfekt aus und beweist nichts.
* `tools/pruefe-geraetediff.mjs` arbeitet deshalb ausdrücklich mit Geräten OHNE
* MAC so, wie sie beim Kunden ankommen.
*
* DIE KASKADE
*
* Wiedererkannt wird über den ersten belastbaren Anhaltspunkt, den ein Gerät
* hergibt. Die Reihenfolge ist nach Verlässlichkeit sortiert, nicht nach
* Bequemlichkeit:
*
* 1. **MAC** wenn ausnahmsweise vorhanden (eigener Adapter, ältere Android-
* Version), ist sie unschlagbar. Nur eben fast nie da.
* 2. **mDNS-Name** Brother HL-L2350DW", „Wohnzimmer-TV". Vom Gerät selbst
* vergeben und über Neustarts hinweg stabil.
* 3. **NetBIOS-Name** dasselbe für Windows-Geräte.
* 4. **Hostname** aus dem DNS.
* 5. **IP + Portmuster** der Rückfallwert. Eine IP allein taugt nicht: im
* DHCP-Bereich wandert sie. Zusammen mit den offenen Ports wird daraus ein
* brauchbarer Fingerabdruck (eine Kamera mit 80/554 an .51 ist mit hoher
* Wahrscheinlichkeit dieselbe wie beim letzten Mal).
*
* Der benutzervergebene Name (`customName`) fließt bewusst NICHT ein: er wird
* am Gerät im Protokoll vergeben und wäre beim nächsten Besuch nur dann da,
* wenn dasselbe Protokoll fortgeschrieben wird.
*/
/** Minimalform eines Geräts, wie sie App und API gleichermaßen liefern */
export interface DiffGeraet {
ip: string;
mac?: string | null;
hostname?: string | null;
netbiosName?: string | null;
mdnsName?: string | null;
openPorts?: number[] | null;
deviceType?: string | null;
vendor?: string | null;
}
/** Wie ein Gerät wiedererkannt wurde — steht so im Kundendokument */
export type Erkennung = 'mac' | 'mdns' | 'netbios' | 'hostname' | 'ip+ports';
export interface DiffEintrag {
ip: string;
/** Anzeigename, so gut wie das Gerät ihn hergibt */
name: string;
erkennung: Erkennung;
/** bei Wiedersehen: die frühere IP, falls sie sich geändert hat */
frühereIp?: string;
}
export interface DiffErgebnis {
neu: DiffEintrag[];
verschwunden: DiffEintrag[];
geblieben: DiffEintrag[];
/** geblieben, aber unter anderer IP */
ipGewechselt: DiffEintrag[];
/**
* Gleiche Adresse, aber anderes Portmuster dasselbe Gerät bietet andere
* Dienste an, oder es sitzt ein anderes Gerät auf der Adresse. Ohne diese
* Kategorie stünde es gleichzeitig unter neu" und „verschwunden".
*/
portsGeaendert: DiffEintrag[];
/**
* Anteil der Geräte, die nur über IP + Portmuster" zugeordnet werden
* konnten (0100). Ab hier wird die Aussage unsicher, und das gehört ins
* Dokument statt in eine Fußnote.
*/
schwacheZuordnungProzent: number;
}
/** Namen normalisieren: Groß/Klein und .local-Endung sind kein Unterschied */
function normName(v: string | null | undefined): string {
return (v ?? '')
.trim()
.toLowerCase()
.replace(/\.local\.?$/, '');
}
/**
* Schlüssel eines Geräts nach der Kaskade plus die Angabe, worauf er beruht.
*/
export function geraeteSchluessel(g: DiffGeraet): { key: string; erkennung: Erkennung } {
const mac = normName(g.mac).replace(/[^0-9a-f]/g, '');
if (mac.length === 12) {
return { key: 'mac:' + mac, erkennung: 'mac' };
}
const mdns = normName(g.mdnsName);
if (mdns) {
return { key: 'mdns:' + mdns, erkennung: 'mdns' };
}
const nb = normName(g.netbiosName);
if (nb) {
return { key: 'nb:' + nb, erkennung: 'netbios' };
}
const hn = normName(g.hostname);
if (hn) {
return { key: 'host:' + hn, erkennung: 'hostname' };
}
// Rückfall: IP + sortiertes Portmuster. Ports sortiert, weil die
// Scan-Reihenfolge nicht garantiert ist.
const ports = [...(g.openPorts ?? [])].sort((a, b) => a - b).join(',');
return { key: `ipp:${g.ip}|${ports}`, erkennung: 'ip+ports' };
}
/** Anzeigename für das Protokoll — der erste, den das Gerät hergibt */
export function anzeigeName(g: DiffGeraet): string {
for (const kandidat of [g.mdnsName, g.netbiosName, g.hostname]) {
const v = (kandidat ?? '').trim();
if (v) return v;
}
return g.ip;
}
/**
* Zwei Gerätelisten vergleichen.
*
* @param vorher Geräte des früheren Protokolls
* @param jetzt Geräte des aktuellen Protokolls
*/
export function vergleicheGeraete(vorher: DiffGeraet[], jetzt: DiffGeraet[]): DiffErgebnis {
const vorherMap = new Map<string, DiffGeraet>();
for (const g of vorher) {
vorherMap.set(geraeteSchluessel(g).key, g);
}
const jetztMap = new Map<string, DiffGeraet>();
for (const g of jetzt) {
jetztMap.set(geraeteSchluessel(g).key, g);
}
const neu: DiffEintrag[] = [];
const geblieben: DiffEintrag[] = [];
const ipGewechselt: DiffEintrag[] = [];
let schwach = 0;
for (const g of jetzt) {
const { key, erkennung } = geraeteSchluessel(g);
if (erkennung === 'ip+ports') schwach++;
const alt = vorherMap.get(key);
const eintrag: DiffEintrag = { ip: g.ip, name: anzeigeName(g), erkennung };
if (!alt) {
neu.push(eintrag);
} else if (alt.ip !== g.ip) {
ipGewechselt.push({ ...eintrag, frühereIp: alt.ip });
} else {
geblieben.push(eintrag);
}
}
const verschwunden: DiffEintrag[] = [];
for (const g of vorher) {
const { key, erkennung } = geraeteSchluessel(g);
if (!jetztMap.has(key)) {
verschwunden.push({ ip: g.ip, name: anzeigeName(g), erkennung });
}
}
/*
* Dieselbe Adresse in neu" UND „verschwunden" das passiert bei einem
* namenlosen Gerät, dessen Portmuster sich geändert hat: der Schlüssel
* `ip+ports` ist dann ein anderer, obwohl die Adresse dieselbe blieb.
*
* Formal ist das richtig, im Kundendokument aber Unsinn: 10.0.2.2 ist neu"
* direkt über 10.0.2.2 ist nicht mehr erreichbar" liest sich wie ein
* kaputtes Werkzeug. Genau so stand es im ersten erzeugten PDF (ND2026-0016).
* Solche Fälle bekommen deshalb eine eigene, ehrliche Kategorie und sie ist
* sogar der interessantere Befund: an dieser Adresse hat sich etwas an den
* angebotenen Diensten geändert.
*/
const portsGeaendert: DiffEintrag[] = [];
const verschwundenIps = new Map(verschwunden.map((e) => [e.ip, e]));
for (let i = neu.length - 1; i >= 0; i--) {
const alt = verschwundenIps.get(neu[i].ip);
if (!alt || alt.erkennung !== 'ip+ports' || neu[i].erkennung !== 'ip+ports') continue;
portsGeaendert.push(neu[i]);
neu.splice(i, 1);
const j = verschwunden.findIndex((e) => e.ip === alt.ip);
if (j >= 0) verschwunden.splice(j, 1);
}
return {
neu,
verschwunden,
geblieben,
ipGewechselt,
portsGeaendert,
schwacheZuordnungProzent: jetzt.length ? Math.round((schwach / jetzt.length) * 100) : 0,
};
}
/**
* Ergebnis bewerten.
*
* Bewusst KEINE Ampel nach viele Änderungen = schlecht": ein neues Gerät im
* Netz ist erst einmal nur eine Tatsache, und der Techniker hat es womöglich
* selbst angeschlossen. Rot gibt es hier nicht Gelb nur, wenn die Aussage
* selbst wackelt oder etwas fehlt, das vorher da war.
*/
export function bewerteDiff(d: DiffErgebnis, vorherAnzahl: number): {
status: 0 | 1 | 2 | 3;
label: string;
hinweis?: string;
} {
if (vorherAnzahl === 0) {
return {
status: 3,
label: 'Kein früheres Protokoll zum Vergleichen',
hinweis:
'Für diesen Kunden liegt kein abgeschlossenes früheres Protokoll vor. Der Vergleich ' +
'ist damit nicht möglich — es wurde nichts über Veränderungen im Netz festgestellt.',
};
}
const teile: string[] = [];
if (d.neu.length) teile.push(`${d.neu.length} neu`);
if (d.verschwunden.length) teile.push(`${d.verschwunden.length} nicht mehr erreichbar`);
if (d.ipGewechselt.length) teile.push(`${d.ipGewechselt.length} mit neuer IP`);
if (d.portsGeaendert.length) teile.push(`${d.portsGeaendert.length} mit anderen offenen Ports`);
const label = teile.length
? `Veränderungen: ${teile.join(', ')} (${d.geblieben.length} unverändert)`
: `Keine Veränderung — alle ${d.geblieben.length} Geräte wie beim letzten Mal`;
const hinweise: string[] = [];
if (d.schwacheZuordnungProzent >= 50) {
hinweise.push(
`Bei ${d.schwacheZuordnungProzent} % der Geräte war keine Kennung verfügbar (kein ` +
'mDNS-, NetBIOS- oder Hostname); sie wurden über IP und Portmuster zugeordnet. ' +
'Wechselt ein Gerät im DHCP-Bereich die Adresse, kann es dabei fälschlich als neu ' +
'gelten. Die Einzelzeilen nennen jeweils die verwendete Kennung.',
);
}
if (d.verschwunden.length) {
hinweise.push(
'Nicht erreichbar heißt nicht zwingend „abgebaut" — ein ausgeschaltetes Gerät, ein ' +
'schlafender Rechner oder eine Kamera im Standby fehlen im Scan genauso.',
);
}
if (d.portsGeaendert.length) {
hinweise.push(
'Bei gleicher Adresse mit anderen offenen Ports kann ein Dienst hinzugekommen oder ' +
'weggefallen sein — oder es sitzt inzwischen ein anderes Gerät auf der Adresse. ' +
'Ohne Gerätenamen lässt sich das nicht unterscheiden.',
);
}
return {
// Gelb nur, wenn etwas fehlt oder die Zuordnung wackelt.
status: d.verschwunden.length || d.schwacheZuordnungProzent >= 50 ? 1 : 0,
label,
hinweis: hinweise.length ? hinweise.join(' ') : undefined,
};
}

View file

@ -1,130 +0,0 @@
/**
* Tool: Geräte-Vergleich was hat sich seit dem letzten Besuch geändert?
*
* Holt das jüngste frühere Protokoll desselben Kunden vom Server und
* vergleicht dessen Geräteliste mit der aktuellen. Beim Wiederholungsbesuch
* ist das die Frage, die der Kunde als Erstes stellt (da ist doch nichts
* dazugekommen, oder?") und die einzige, die ein einzelnes Protokoll nicht
* beantworten kann.
*
* Die Wiedererkennung läuft NICHT über MAC-Adressen; warum, steht ausführlich
* im Kopf von `geraete-diff.ts`. Kurz: `/proc/net/arp` ist ab Android 10
* gesperrt, im ausgelieferten Protokoll ND2026-0015 hatten alle 68 Geräte
* keine MAC. Ein MAC-Diff meldete dort bei jedem Besuch alles neu".
*
* Braucht Serververbindung. Ohne die gibt es kein Vergleichsprotokoll dann
* meldet das Werkzeug nicht messbar" statt zu raten.
*/
import { getCustomer, getProtocol } from '../../api';
import type { Tool } from '../types';
import { bewerteDiff, vergleicheGeraete, type DiffGeraet } from './geraete-diff';
export { bewerteDiff, vergleicheGeraete, geraeteSchluessel } from './geraete-diff';
export type { DiffGeraet, DiffErgebnis, DiffEintrag, Erkennung } from './geraete-diff';
/** Erkennungsart in Klartext fürs Kundendokument */
const ERKENNUNG_TEXT: Record<string, string> = {
mac: 'MAC-Adresse',
mdns: 'Netzwerkname (mDNS)',
netbios: 'Windows-Name',
hostname: 'Hostname',
'ip+ports': 'IP und Portmuster',
};
export const geraeteDiffTool: Tool = {
id: 'geraetediff',
category: 'netzwerk',
name: 'Geräte-Vergleich',
icon: 'git-compare',
description: 'Vergleicht die Geräteliste mit dem letzten Protokoll dieses Kunden.',
scope: 'protocol',
quickRun: true,
params: [],
async run(ctx) {
const socId = ctx.protocol.socId;
if (!socId) {
return {
label: 'Kein Kunde am Protokoll — kein Vergleich möglich',
measureStatus: 3,
result: {
hinweis:
'Das Protokoll ist keinem Kunden zugeordnet. Der Vergleich braucht die ' +
'früheren Protokolle desselben Kunden.',
},
};
}
// Frühere Protokolle des Kunden holen. Das aktuelle selbst fliegt raus —
// sonst verglichen wir es mit sich selbst und meldeten stets „keine
// Veränderung".
const { protocols } = await getCustomer(socId);
const frühere = (protocols ?? [])
.filter((p) => p.clientUuid !== ctx.protocol.clientUuid && p.id !== ctx.protocol.serverId)
// Nur Protokolle mit Geräten taugen als Vergleich; ein leerer Entwurf
// vom selben Tag würde sonst „alle Geräte neu" ergeben.
.filter((p) => (p.deviceCount ?? 0) > 0)
.sort((a, b) => (b.dateDiag ?? 0) - (a.dateDiag ?? 0));
const vorgänger = frühere[0];
if (!vorgänger) {
const urteil = bewerteDiff(vergleicheGeraete([], []), 0);
return {
label: urteil.label,
measureStatus: urteil.status,
result: { hinweis: urteil.hinweis },
};
}
const alt = await getProtocol(vorgänger.id);
const vorher: DiffGeraet[] = (alt.devices ?? []).map((d) => ({
ip: d.ip,
mac: d.mac,
hostname: d.hostname,
netbiosName: d.netbiosName,
mdnsName: d.mdnsName,
openPorts: d.openPorts,
deviceType: d.deviceType,
}));
const jetzt: DiffGeraet[] = ctx.protocol.devices.map((d) => ({
ip: d.ip,
mac: d.mac,
hostname: d.hostname,
netbiosName: d.netbiosName,
mdnsName: d.mdnsName,
openPorts: d.openPorts,
deviceType: d.deviceType,
}));
const diff = vergleicheGeraete(vorher, jetzt);
const urteil = bewerteDiff(diff, vorher.length);
const zeile = (e: { name: string; ip: string; erkennung: string; frühereIp?: string }) =>
`${e.name === e.ip ? e.ip : `${e.name} (${e.ip})`}` +
(e.frühereIp ? ` — vorher ${e.frühereIp}` : '') +
` · erkannt über ${ERKENNUNG_TEXT[e.erkennung] ?? e.erkennung}`;
const vergleichsDatum = vorgänger.dateDiag
? new Date(vorgänger.dateDiag * 1000).toLocaleDateString('de-DE')
: '';
return {
label: urteil.label,
measureStatus: urteil.status,
result: {
verglichenMit: `${vorgänger.ref ?? 'Protokoll'}${vergleichsDatum ? ' vom ' + vergleichsDatum : ''}`,
geraeteVorher: vorher.length,
geraeteJetzt: jetzt.length,
neu: diff.neu.map(zeile),
nichtMehrErreichbar: diff.verschwunden.map(zeile),
mitNeuerAdresse: diff.ipGewechselt.map(zeile),
andereOffenePorts: diff.portsGeaendert.map(zeile),
unveraendert: diff.geblieben.length,
hinweis: urteil.hinweis,
verfahren:
'Wiedererkennung über Netzwerkname, Windows-Name, Hostname oder IP und Portmuster — ' +
'nicht über die MAC-Adresse, die Android seit Version 10 nicht mehr herausgibt.',
},
};
},
};

View file

@ -6,7 +6,6 @@
import { Capacitor } from '@capacitor/core';
import { sync } from '$lib/sync.svelte';
import { toast } from '$lib/toast.svelte';
import { pruefeServerAdresse } from '$lib/serveradresse';
import { APP_VERSION, checkForUpdate, installUpdate, type UpdateInfo } from '$lib/updater';
import { ladeDemoSchalter, setzeDemoSchalter } from '$lib/wifi/demo';
import { onMount } from 'svelte';
@ -94,12 +93,12 @@
*/
async function saveServer() {
const url = serverInput.trim();
// Prüfung ausgelagert und einzeln nachrechenbar (tools/pruefe-serveradresse.mjs).
// Sie weist unter anderem http://-Adressen ab, solange die App selbst über
// https läuft — die würden sonst stumm scheitern, siehe serveradresse.ts.
const pruefung = pruefeServerAdresse(url, window.location.protocol);
if (!pruefung.ok) {
toast.show(pruefung.meldung, 'error', 8000);
if (!url) {
toast.show('Server-Adresse fehlt', 'error');
return;
}
if (!/^https?:\/\//i.test(url)) {
toast.show('Adresse muss mit http:// oder https:// beginnen', 'error', 5000);
return;
}
await setServerUrl(url);

View file

@ -1,113 +0,0 @@
/**
* Prueft den Geraete-Vergleich zweier Protokolle.
*
* ALLE Faelle arbeiten mit Geraeten OHNE MAC - genau so kommen sie beim Kunden
* an (/proc/net/arp ist ab Android 10 gesperrt; im ausgelieferten Protokoll
* ND2026-0015 waren bei allen 68 Geraeten MAC, Hostname und Hersteller leer).
* Der Browser-Mock dagegen gibt jedem Geraet eine MAC - dort saehe auch ein
* naiver MAC-Diff perfekt aus und wuerde nichts beweisen.
*
* Start: node tools/pruefe-geraetediff.mjs
*/
const { vergleicheGeraete, bewerteDiff, geraeteSchluessel } =
await import('../src/lib/tools/netzwerk/geraete-diff.ts');
// Geraete wie sie real ankommen: keine MAC, oft kein Name.
const kamera = { ip: '192.168.1.51', mdnsName: 'IPC-Hof', openPorts: [80, 554] };
const drucker = { ip: '192.168.1.20', mdnsName: 'Brother HL-L2350DW', openPorts: [80, 9100, 515] };
const pc = { ip: '192.168.1.30', netbiosName: 'BUERO-PC', openPorts: [139, 445] };
const nas = { ip: '192.168.1.10', hostname: 'nas.fritz.box', openPorts: [80, 445, 5000] };
const namenlos= { ip: '192.168.1.77', openPorts: [502] }; // Wechselrichter, Modbus
const router = { ip: '192.168.1.1', hostname: 'fritz.box', openPorts: [80, 443, 53] };
let fehler = 0;
function pruefe(name, ist, soll) {
const ok = JSON.stringify(ist) === JSON.stringify(soll);
if (!ok) fehler++;
console.log(`${ok ? 'OK ' : 'FEHL'} ${name}`);
if (!ok) console.log(` ist=${JSON.stringify(ist)} soll=${JSON.stringify(soll)}`);
}
// 1. Identisch -> keine Veraenderung
let d = vergleicheGeraete([router, nas, kamera], [router, nas, kamera]);
pruefe('Unveraendertes Netz -> 0 neu, 0 weg, 3 geblieben',
[d.neu.length, d.verschwunden.length, d.geblieben.length], [0, 0, 3]);
// 2. DER KERNFALL: dieselben Geraete OHNE MAC, aber mit neuen DHCP-Adressen.
// Ein MAC-Diff haette hier "alles neu, alles verschwunden" gemeldet.
const kameraNeueIp = { ...kamera, ip: '192.168.1.99' };
const druckerNeueIp = { ...drucker, ip: '192.168.1.98' };
d = vergleicheGeraete([kamera, drucker], [kameraNeueIp, druckerNeueIp]);
pruefe('Beide Geraete mit neuer IP -> 0 neu, 0 weg, 2 mit IP-Wechsel',
[d.neu.length, d.verschwunden.length, d.ipGewechselt.length], [0, 0, 2]);
pruefe('IP-Wechsel nennt die fruehere Adresse',
d.ipGewechselt.map((e) => e.frühereIp).sort(), ['192.168.1.20', '192.168.1.51']);
// 3. Echtes neues Geraet
d = vergleicheGeraete([router, nas], [router, nas, kamera]);
pruefe('Ein Geraet dazugekommen', [d.neu.length, d.neu[0]?.name], [1, 'IPC-Hof']);
// 4. Geraet verschwunden
d = vergleicheGeraete([router, nas, kamera], [router, nas]);
pruefe('Ein Geraet verschwunden', [d.verschwunden.length, d.verschwunden[0]?.name], [1, 'IPC-Hof']);
// 5. Namenloses Geraet: gleiche IP + gleiche Ports -> wiedererkannt
d = vergleicheGeraete([namenlos], [{ ...namenlos }]);
pruefe('Namenlos, IP+Ports gleich -> wiedererkannt', [d.geblieben.length, d.neu.length], [1, 0]);
// 6. Namenloses Geraet mit neuer IP -> NICHT zuordenbar, ehrlich als neu+weg
d = vergleicheGeraete([namenlos], [{ ...namenlos, ip: '192.168.1.78' }]);
pruefe('Namenlos mit neuer IP -> gilt als neu UND verschwunden (Grenze des Verfahrens)',
[d.neu.length, d.verschwunden.length], [1, 1]);
// 6b. DER FALL AUS DEM ERSTEN PDF: gleiche IP, anderes Portmuster.
// Darf NICHT gleichzeitig als neu und verschwunden erscheinen.
d = vergleicheGeraete(
[{ ip: '10.0.2.2', openPorts: [80] }],
[{ ip: '10.0.2.2', openPorts: [80, 443] }],
);
pruefe('Gleiche IP, andere Ports -> eigene Kategorie statt "neu UND weg"',
[d.neu.length, d.verschwunden.length, d.portsGeaendert.length], [0, 0, 1]);
// 7. Erkennungsart wird korrekt benannt
pruefe('Kaskade: mDNS schlaegt Hostname',
geraeteSchluessel({ ip: '1.2.3.4', mdnsName: 'A', hostname: 'B' }).erkennung, 'mdns');
pruefe('Kaskade: NetBIOS vor Hostname',
geraeteSchluessel({ ip: '1.2.3.4', netbiosName: 'A', hostname: 'B' }).erkennung, 'netbios');
pruefe('Kaskade: ohne alles -> ip+ports',
geraeteSchluessel({ ip: '1.2.3.4', openPorts: [80] }).erkennung, 'ip+ports');
pruefe('Kaskade: MAC schlaegt alles (falls ausnahmsweise da)',
geraeteSchluessel({ ip: '1.2.3.4', mac: 'AA:BB:CC:11:22:33', mdnsName: 'A' }).erkennung, 'mac');
// 8. Schreibweise: Gross/Klein und .local duerfen kein Unterschied sein
d = vergleicheGeraete(
[{ ip: '10.0.0.5', mdnsName: 'Drucker.local' }],
[{ ip: '10.0.0.5', mdnsName: 'drucker' }],
);
pruefe('mDNS "Drucker.local" == "drucker"', [d.geblieben.length, d.neu.length], [1, 0]);
// 9. Portmuster-Reihenfolge darf egal sein
d = vergleicheGeraete(
[{ ip: '10.0.0.9', openPorts: [443, 80] }],
[{ ip: '10.0.0.9', openPorts: [80, 443] }],
);
pruefe('Portreihenfolge egal', [d.geblieben.length, d.neu.length], [1, 0]);
// 10. Bewertung
let b = bewerteDiff(vergleicheGeraete([router, nas], [router, nas]), 2);
pruefe('Keine Veraenderung -> GRUEN', b.status, 0);
b = bewerteDiff(vergleicheGeraete([router, nas, kamera], [router, nas]), 3);
pruefe('Geraet fehlt -> GELB mit Einschraenkung', [b.status, /Standby/.test(b.hinweis || '')], [1, true]);
b = bewerteDiff(vergleicheGeraete([], []), 0);
pruefe('Kein Vorgaengerprotokoll -> NICHT MESSBAR', b.status, 3);
b = bewerteDiff(vergleicheGeraete([namenlos], [namenlos]), 1);
pruefe('Nur schwache Zuordnung -> GELB mit Warnung', [b.status, /IP und Portmuster/.test(b.hinweis || '')], [1, true]);
b = bewerteDiff(vergleicheGeraete([router, nas], [router, nas, kamera]), 2);
pruefe('Nur neue Geraete -> GRUEN (neu ist kein Fehler)', b.status, 0);
b = bewerteDiff(vergleicheGeraete(
[{ ip: '10.0.2.2', openPorts: [80] }],
[{ ip: '10.0.2.2', openPorts: [80, 443] }]), 1);
pruefe('Portwechsel wird im Label genannt', /andere.*Ports/.test(b.label), true);
console.log(fehler === 0 ? 'ALLE FAELLE KORREKT' : `${fehler} FEHLER`);
process.exit(fehler === 0 ? 0 : 1);

View file

@ -1,39 +0,0 @@
/**
* Prueft die Validierung der Server-Adresse.
*
* Kern: eine http://-Adresse wird abgewiesen, solange die App selbst ueber
* https laeuft - sonst scheitert sie beim Kunden stumm mit "Keine Verbindung
* zum Server". Ein Debug-Build auf http://localhost darf HTTP dagegen sehr
* wohl, sonst waere ausgerechnet der Testbau unbenutzbar.
*
* Start: node tools/pruefe-serveradresse.mjs
*/
const { pruefeServerAdresse } = await import('../src/lib/serveradresse.ts');
const HTTPS = 'https:';
const HTTP = 'http:';
const faelle = [
{ e: 'https://awl.data-it-solution.de', p: HTTPS, ok: true, name: 'Prod-Adresse ueber https' },
{ e: 'https://10.0.2.2:8443', p: HTTPS, ok: true, name: 'IP mit Port ueber https' },
{ e: 'http://10.0.2.2:8080', p: HTTPS, ok: false, name: 'HTTP bei https-App -> ABGEWIESEN (der eigentliche Fall)' },
{ e: 'http://10.0.2.2:8080', p: HTTP, ok: true, name: 'HTTP bei http-App (Debug-Build) -> ERLAUBT' },
{ e: 'HTTP://10.0.2.2:8080', p: HTTPS, ok: false, name: 'Grossschreibung wird auch erkannt' },
{ e: ' https://awl.example ', p: HTTPS, ok: true, name: 'Leerzeichen aussen stoeren nicht' },
{ e: '', p: HTTPS, ok: false, name: 'Leer' },
{ e: ' ', p: HTTPS, ok: false, name: 'Nur Leerzeichen' },
{ e: 'awl.data-it-solution.de', p: HTTPS, ok: false, name: 'Ohne Schema' },
{ e: 'ftp://server', p: HTTPS, ok: false, name: 'Falsches Schema' },
{ e: 'https://', p: HTTPS, ok: false, name: 'Schema ohne Host' },
];
let fehler = 0;
for (const f of faelle) {
const r = pruefeServerAdresse(f.e, f.p);
const okk = r.ok === f.ok;
if (!okk) fehler++;
console.log(`${okk ? 'OK ' : 'FEHL'} ${f.name}`);
if (!r.ok) console.log(` -> ${r.meldung.slice(0, 90)}`);
}
console.log(fehler === 0 ? 'ALLE FAELLE KORREKT' : `${fehler} FEHLER`);
process.exit(fehler === 0 ? 0 : 1);