Compare commits

..

3 commits

Author SHA1 Message Date
28a7fa9f79 Trigger: Modul-Deploy [deploy]
All checks were successful
Deploy netdiag / deploy (push) Successful in 13s
2026-08-16 19:45:38 +02:00
a39688a74e Monitor-Felder in die Kundenfeld-Whitelist
Der Geraete-Monitor liefert ab sofort eine richtige Messung (App-Seite).
Ohne diese sechs Schluessel stuende im Kunden-PDF nur die Ampel — genau der
Fehler, der bei der Whitelist schon einmal passiert ist.

geraeteAnzahl, verfuegbarkeitProzent, ausfallzeitSek, aussetzer, geraete,
ausfaelle. Die Werte sind der Beleg fuer "das Netz war ueber X Stunden stabil"
bzw. fuer die haeufigste Reklamation ("die Kamera faellt staendig aus").
2026-08-16 19:28:57 +02:00
3d12ee1e61 Vier Bestandsfehler im Kundendokument und im Sync behoben
Alle vier durch eine systematische Gegenpruefung des echten Codes gefunden,
nicht durch Symptome — sie waren bereits ausgeliefert und still.

1. Whitelist-Schluessel 'typ' existiert nicht. Die App liefert das
   LinkInfo-Objekt durch, dort heisst das Feld 'type' (app/src/lib/types.ts).
   Folge: im Kundendokument fehlte ausgerechnet beim IP-Test an der Dose die
   Angabe, ob per LAN oder WLAN gemessen wurde - bei einem Abnahmebeleg die
   halbe Aussage. Zusaetzlich 'rssi' aufgenommen: bei einer WLAN-Dose IST der
   Empfangspegel der Messwert, er wurde bisher verworfen.
   Die Rohwerte sind englisch; netdiagKundentext() bildet sie jetzt ab
   ('ethernet' -> 'LAN (Kabel)'), sonst stuende "Anschlussart: ethernet" im
   Abnahmeprotokoll eines deutschen Handwerksbetriebs.

2. Leeres Array druckte "Offene Ports: " ohne Wert. Die Leerpruefung ist ein
   elseif hinter dem Array-Zweig und wurde nie erreicht. Betraf ausgerechnet
   die GUTEN Ergebnisse: Portscan ohne offenen Port, IP-Konflikt ohne
   Konflikt, IP-Scan ohne Veraenderung. Jetzt "keine" - Weglassen waere
   schlechter, weil der Kunde sonst nicht unterscheiden kann, ob nichts
   gefunden oder nichts geprueft wurde.

3. 'error' fehlte in der Whitelist. Der IP-Konflikt gibt im Abbruchfall
   { error: ... } mit roter Ampel zurueck - im Kundendokument stand eine rote
   Ampel ohne einen Buchstaben Erklaerung. Die App vereinheitlicht kuenftig
   auf 'fehler', aber Altdaten lassen sich nicht aendern.

4. tool/category/label wurden beim Sync nicht auf die Spaltenlaenge gekuerzt
   (varchar 64/32/255). Bei striktem SQL-Modus kippt EIN zu langes Label den
   gesamten Sync per rollback() - der Techniker steht beim Kunden mit einem
   Protokoll da, das sich nicht abschliessen laesst, weil eine Beschriftung zu
   lang war. Die App setzt Labels durch Verketten zusammen, 255 Zeichen sind
   erreichbar. found_via wurde 20 Zeilen darueber laengst gekappt.

Ausserdem:
- GET-Zweig der API prueft jetzt eine Berechtigung. Bisher konnte jeder
  angemeldete Benutzer mit einer geratenen ID ein fremdes Protokoll samt
  Geraeteliste, IP-Adressen und offenen Ports abrufen. 'write' wird bewusst
  mit akzeptiert: die Rechte sind in Dolibarr einzeln vergebbar, und ein
  Techniker mit Schreib- ohne ausdruecklichem Leserecht duerfte nicht
  ausgesperrt werden - das waere erst beim Kunden aufgefallen.
- netdiagDauerLesbar: "1 Tage 0 Std" -> "1 Tag". floor() liefert einen Float,
  1.0 === 1 ist in PHP false.

Geprueft: php -l, und netdiagFeldFuerKunde() mit 13 echten Faellen gegen die
lokale Instanz durchgerechnet (LAN/WLAN/Mobilfunk, leere und gefuellte Arrays,
error/fehler, tote Schluessel, Interna).
2026-08-16 19:02:11 +02:00
2 changed files with 91 additions and 6 deletions

View file

@ -42,6 +42,21 @@ require_once __DIR__.'/../class/netdiagmeasurement.class.php';
// GET: einzelnes Protokoll mit Geräten und Messungen // GET: einzelnes Protokoll mit Geräten und Messungen
// ========================================================================= // =========================================================================
if ($_SERVER['REQUEST_METHOD'] === 'GET') { if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Leserecht prüfen. Der POST-Zweig weiter unten verlangt seit jeher
// 'protocol write', der GET-Zweig prüfte bis 16.08.2026 gar nichts: jeder
// angemeldete Benutzer konnte mit einer geratenen ID ein fremdes Protokoll
// samt Geräteliste, IP-Adressen und offenen Ports abrufen. Das ist beim
// Kunden erhobene Netzstruktur — sie gehört hinter ein Recht.
//
// 'write' wird bewusst mit akzeptiert: In Dolibarr sind die Rechte einzeln
// vergebbar, ein Techniker-Benutzer mit Schreib- aber ohne ausdrückliches
// Leserecht ist möglich. Der würde sonst genau die Protokolle nicht mehr
// abrufen können, die er selbst hochgeladen hat — die App wäre nach dem
// Update kaputt, und zwar erst beim Kunden. Wer schreiben darf, darf lesen.
if (!$user->hasRight('netdiag', 'protocol', 'read')
&& !$user->hasRight('netdiag', 'protocol', 'write')) {
netdiag_api_error('Keine Leseberechtigung', 403);
}
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0; $id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if ($id <= 0) { if ($id <= 0) {
netdiag_api_error('Parameter id fehlt', 400); netdiag_api_error('Parameter id fehlt', 400);
@ -217,9 +232,17 @@ foreach ($measIn as $m) {
$meas->fk_protocol = $protocolId; $meas->fk_protocol = $protocolId;
$dcid = isset($m['deviceClientId']) ? (string) $m['deviceClientId'] : ''; $dcid = isset($m['deviceClientId']) ? (string) $m['deviceClientId'] : '';
$meas->fk_device = ($dcid !== '' && isset($deviceIdMap[$dcid])) ? $deviceIdMap[$dcid] : null; $meas->fk_device = ($dcid !== '' && isset($deviceIdMap[$dcid])) ? $deviceIdMap[$dcid] : null;
$meas->tool = isset($m['tool']) ? (string) $m['tool'] : 'unknown'; // Auf die Spaltenlängen kürzen (varchar 64/32/255, siehe
$meas->category = isset($m['category']) ? (string) $m['category'] : ''; // sql/llx_netdiag_measurement.sql). Ohne das kippt bei striktem SQL-Modus
$meas->label = isset($m['label']) ? (string) $m['label'] : ''; // EIN zu langes Label den gesamten Sync: create() schlägt fehl, es folgt
// rollback() — und der Techniker steht beim Kunden mit einem Protokoll da,
// das sich nicht abschließen lässt, obwohl nur eine Beschriftung zu lang
// war. Die App setzt Labels durch Verketten zusammen (Raumname + SSID +
// Geschwindigkeit …), 255 Zeichen sind dabei erreichbar. Bei den Geräten
// wird längst so verfahren (found_via, netdiag_join_list weiter oben).
$meas->tool = substr(isset($m['tool']) ? (string) $m['tool'] : 'unknown', 0, 64);
$meas->category = substr(isset($m['category']) ? (string) $m['category'] : '', 0, 32);
$meas->label = substr(isset($m['label']) ? (string) $m['label'] : '', 0, 255);
$meas->params = isset($m['params']) ? json_encode($m['params'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null; $meas->params = isset($m['params']) ? json_encode($m['params'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
$meas->result = isset($m['result']) ? json_encode($m['result'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null; $meas->result = isset($m['result']) ? json_encode($m['result'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
// Nur bekannte Bewertungen uebernehmen. Ein unbekannter Wert landete sonst // Nur bekannte Bewertungen uebernehmen. Ein unbekannter Wert landete sonst

View file

@ -332,6 +332,16 @@ function netdiagKundenfelder()
'dauerSekunden' => array('Messdauer', 's'), 'dauerSekunden' => array('Messdauer', 's'),
'intervallSek' => array('Messabstand', 's'), 'intervallSek' => array('Messabstand', 's'),
'laengsterAusfallSek' => array('Längster Ausfall', 's'), 'laengsterAusfallSek' => array('Längster Ausfall', 's'),
// Geräte-Monitor (Dauerüberwachung). Das ist der Beleg für „das Netz
// war über X Stunden stabil" bzw. für die häufigste Reklamation
// („die Kamera fällt ständig aus") — die Felder gehören deshalb
// vollständig ins Kundendokument, nicht nur die Ampel.
'geraeteAnzahl' => array('Überwachte Geräte', ''),
'verfuegbarkeitProzent' => array('Verfügbarkeit (schlechtestes Gerät)', '%'),
'ausfallzeitSek' => array('Ausfallzeit', 's'),
'aussetzer' => array('Aussetzer', ''),
'geraete' => array('Je Gerät', ''),
'ausfaelle' => array('Einzelne Ausfälle', ''),
// Netz/DHCP // Netz/DHCP
'server' => array('DHCP-Server', ''), 'server' => array('DHCP-Server', ''),
'lease' => array('Lease-Dauer', 's'), 'lease' => array('Lease-Dauer', 's'),
@ -378,14 +388,32 @@ function netdiagKundenfelder()
'ssid' => array('WLAN-Name', ''), 'ssid' => array('WLAN-Name', ''),
'raum' => array('Raum / Dose', ''), 'raum' => array('Raum / Dose', ''),
'iface' => array('Schnittstelle', ''), 'iface' => array('Schnittstelle', ''),
'typ' => array('Anschlussart', ''), // Anschlussart. ACHTUNG: hier stand bis 16.08.2026 'typ' — ein Schlüssel,
// den kein einziges Werkzeug erzeugt. Die App liefert das LinkInfo-Objekt
// durch, und dort heißt das Feld 'type' (app/src/lib/types.ts). Folge:
// im Kundendokument fehlte ausgerechnet beim IP-Test an der Dose die
// Angabe, ob per LAN oder WLAN gemessen wurde — bei einem Abnahmebeleg
// die halbe Aussage. Rohwerte sind englisch und werden in
// netdiagKundentext() übersetzt.
'type' => array('Anschlussart', ''),
// Empfangspegel: bei einer WLAN-Dose IST das der Messwert. Wurde bisher
// verworfen, weil nicht in der Liste.
'rssi' => array('Empfangspegel', 'dBm'),
'neu' => array('Neu hinzugekommen', ''), 'neu' => array('Neu hinzugekommen', ''),
'nichtMehrErreichbar' => array('Nicht mehr erreichbar', ''), 'nichtMehrErreichbar' => array('Nicht mehr erreichbar', ''),
'demodaten' => array('DEMODATEN — keine echte Messung', ''), 'demodaten' => array('DEMODATEN — keine echte Messung', ''),
// Bewusst NICHT aufgenommen (Messgeräte-Interna, für den Kunden ohne
// Aussage): 'bssid', 'isUsbEthernet', 'isDefault', 'dns'.
// Fehler/Hinweise — immer zeigen, sonst bliebe eine gelbe oder rote Ampel // Fehler/Hinweise — immer zeigen, sonst bliebe eine gelbe oder rote Ampel
// im Kundendokument ohne jede Erklärung stehen. Der Text läuft durch // im Kundendokument ohne jede Erklärung stehen. Der Text läuft durch
// netdiagKundentext() und wird dort von Messgeräte-Interna befreit. // netdiagKundentext() und wird dort von Messgeräte-Interna befreit.
'fehler' => array('Fehler', ''), 'fehler' => array('Fehler', ''),
// 'error' ist die englische Schreibweise desselben Feldes. Die App
// vereinheitlicht auf 'fehler', aber in bereits ausgelieferten
// Protokollen steht 'error' — und Altdaten lassen sich nicht ändern.
// Ohne diesen Eintrag stünde dort eine rote Ampel ohne einen Buchstaben
// Erklärung.
'error' => array('Fehler', ''),
'hinweis' => array('Hinweis', ''), 'hinweis' => array('Hinweis', ''),
'kundenhinweis' => array('Hinweis', ''), 'kundenhinweis' => array('Hinweis', ''),
); );
@ -407,9 +435,18 @@ function netdiagDauerLesbar($sek)
return round($sek / 60).' min'; return round($sek / 60).' min';
} }
if ($sek < 86400) { if ($sek < 86400) {
return floor($sek / 3600).' Std '.round(($sek % 3600) / 60).' min'; $std = floor($sek / 3600);
$min = round(($sek % 3600) / 60);
// Volle Stunde nicht als „2 Std 0 min" ausgeben
return $std.' Std'.($min > 0 ? ' '.$min.' min' : '');
} }
return floor($sek / 86400).' Tage '.round(($sek % 86400) / 3600).' Std'; // (int) nicht weglassen: floor() liefert einen Float, und 1.0 === 1 ist in
// PHP false — die Einzahl unten hätte sonst nie gegriffen.
$tage = (int) floor($sek / 86400);
$std = (int) round(($sek % 86400) / 3600);
// Einzahl/Mehrzahl und die glatte Angabe: eine DHCP-Lease von genau einem
// Tag las sich vorher als „1 Tage 0 Std".
return $tage.($tage === 1 ? ' Tag' : ' Tage').($std > 0 ? ' '.$std.' Std' : '');
} }
/** /**
@ -434,6 +471,16 @@ function netdiagFeldFuerKunde($key, $val)
$flat[] = is_array($item) ? json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $item; $flat[] = is_array($item) ? json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $item;
} }
$val = implode(', ', $flat); $val = implode(', ', $flat);
// Leeres Array = das gute Ergebnis: kein offener Port, kein IP-Konflikt,
// keine Veränderung seit dem letzten Scan. Bis 16.08.2026 stand hier im
// Kundendokument „Offene Ports: " ohne Wert — die Leerprüfung weiter
// unten ist ein elseif und wurde nach diesem Zweig nie erreicht.
// „keine" statt Weglassen, weil die Zeile sonst kommentarlos
// verschwindet und der Kunde nicht unterscheiden kann, ob nichts
// gefunden oder nichts geprüft wurde.
if ($val === '') {
$val = 'keine';
}
} elseif (is_bool($val)) { } elseif (is_bool($val)) {
$val = $val ? 'ja' : 'nein'; $val = $val ? 'ja' : 'nein';
} elseif ($val === null || $val === '') { } elseif ($val === null || $val === '') {
@ -466,6 +513,21 @@ function netdiagKundentext($text)
if (stripos($text, '/proc/net/arp') !== false || stripos($text, 'braucht root') !== false) { if (stripos($text, '/proc/net/arp') !== false || stripos($text, 'braucht root') !== false) {
return 'Mit dem eingesetzten Messgerät nicht prüfbar — die Aussage bleibt offen.'; return 'Mit dem eingesetzten Messgerät nicht prüfbar — die Aussage bleibt offen.';
} }
// Anschlussart: Android liefert die Rohwerte englisch. Im Abnahmeprotokoll
// eines deutschen Handwerksbetriebs hat „Anschlussart: ethernet" nichts zu
// suchen. Exakter Vergleich statt stripos, damit nicht versehentlich ein
// Fließtext getroffen wird, in dem das Wort vorkommt.
$anschluss = array(
'wifi' => 'WLAN',
'ethernet' => 'LAN (Kabel)',
'cellular' => 'Mobilfunk',
'vpn' => 'VPN',
'other' => 'sonstige',
);
$k = strtolower(trim($text));
if (isset($anschluss[$k])) {
return $anschluss[$k];
}
return $text; return $text;
} }