* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file htdocs/custom/netdiag/lib/netdiag.lib.php * \ingroup netdiag * \brief Hilfsfunktionen für das Modul NetDiag */ /** * Tabs für die Admin-Seiten des Moduls vorbereiten * * @return array> Tab-Array */ function netdiagAdminPrepareHead() { global $langs, $conf; $langs->load("netdiag@netdiag"); $h = 0; $head = array(); $head[$h][0] = dol_buildpath("/netdiag/admin/setup.php", 1); $head[$h][1] = $langs->trans("Settings"); $head[$h][2] = 'settings'; $h++; $head[$h][0] = dol_buildpath("/netdiag/admin/about.php", 1); $head[$h][1] = $langs->trans("About"); $head[$h][2] = 'about'; $h++; complete_head_from_modules($conf, $langs, null, $head, $h, 'netdiag@netdiag'); complete_head_from_modules($conf, $langs, null, $head, $h, 'netdiag@netdiag', 'remove'); return $head; } /** * Tabs für die Detailansicht eines Diagnose-Protokolls vorbereiten * * @param NetDiagProtocol $object Protokoll-Objekt * @return array> Tab-Array */ function netdiagProtocolPrepareHead($object) { global $langs, $conf; $langs->load("netdiag@netdiag"); $h = 0; $head = array(); $head[$h][0] = dol_buildpath("/netdiag/netdiagprotocol_card.php", 1).'?id='.$object->id; $head[$h][1] = $langs->trans("NetDiagProtocol"); $head[$h][2] = 'card'; $h++; complete_head_from_modules($conf, $langs, $object, $head, $h, 'netdiagprotocol@netdiag'); complete_head_from_modules($conf, $langs, $object, $head, $h, 'netdiagprotocol@netdiag', 'remove'); return $head; } /** * Ausgabeverzeichnis des Moduls ermitteln (für PDF-Dokumente) * * @return string Absoluter Pfad zum Dokumentenverzeichnis */ function netdiagGetOutputDir() { global $conf; if (!empty($conf->netdiag->dir_output)) { return $conf->netdiag->dir_output; } return DOL_DATA_ROOT.'/netdiag'; } /** * Ein Mess-Ergebnis (JSON) lesbar als HTML aufbereiten. * * Generisch: rendert flache Schlüssel/Wert-Paare und einfache Listen, * damit auch künftige Tools ohne Code-Änderung dargestellt werden. Jeder Wert * wird auf 200 Zeichen gekürzt (`dol_trunc`) — für Skalare unschädlich, aber * ein Dauertest mit vielen Ausfällen/Minuten-Buckets würde dabei mitten im * Satz abgeschnitten. Für `$tool === 'stresstest'` deshalb eigener, nicht * gekürzter Zweig (siehe netdiagFormatStressTest) — dieselbe Notlösung wie * netdiagPdfStressTest() im PDF-Generator. * * @param string $json JSON-String des Ergebnisses * @param string $tool Werkzeug-ID der Messung (optional, für Sonderfälle) * @return string HTML-Schnipsel */ function netdiagFormatResult($json, $tool = '') { if (empty($json)) { return '-'; } $data = json_decode($json, true); if ($data === null) { return dol_escape_htmltag(dol_trunc($json, 120)); } if (!is_array($data)) { return dol_escape_htmltag((string) $data); } if ($tool === 'stresstest') { return netdiagFormatStressTest($data); } if ($tool === 'wifikanal') { return netdiagFormatWifiKanal($data); } $out = '
'; foreach ($data as $key => $val) { $label = dol_escape_htmltag(ucfirst((string) $key)); if (is_array($val)) { $flat = array(); foreach ($val as $item) { $flat[] = is_array($item) ? json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $item; } $valstr = implode(', ', $flat); } elseif (is_bool($val)) { $valstr = $val ? 'ja' : 'nein'; } else { $valstr = (string) $val; } $out .= ''.$label.': '.dol_escape_htmltag(dol_trunc($valstr, 200)).' '; } $out .= '
'; return $out; } /** * Dauer-/Stresstest-Ergebnis strukturiert als HTML aufbereiten (Kennzahlen + * Ausfallliste, ungekürzt) statt es wie ein generisches Werkzeug zu * behandeln — sonst schneidet dol_trunc(...,200) die Ausfallliste eines * längeren Laufs mitten im Satz ab. * * @param array $data bereits dekodiertes result-JSON * @return string HTML-Schnipsel */ function netdiagFormatStressTest($data) { if (!empty($data['hinweis']) || !empty($data['fehler'])) { return ''.dol_escape_htmltag((string) ($data['hinweis'] ?? $data['fehler'])).''; } $sum = array(); if (isset($data['host'])) { $sum[] = 'Ziel: '.$data['host']; } if (isset($data['dauerSekunden'])) { $sum[] = 'Dauer: '.round($data['dauerSekunden'] / 60).' min'; } if (isset($data['intervallSek'])) { $sum[] = 'Takt: '.$data['intervallSek'].' s'; } if (isset($data['gesendet']) && isset($data['empfangen'])) { $sum[] = 'Proben: '.$data['empfangen'].'/'.$data['gesendet']; } if (isset($data['verlustProzent'])) { $sum[] = 'Verlust: '.$data['verlustProzent'].' %'; } if (isset($data['avgMs']) && $data['avgMs'] !== null) { $sum[] = 'Ø '.$data['avgMs'].' ms'; } if (isset($data['minMs']) && isset($data['maxMs']) && $data['minMs'] !== null) { $sum[] = 'Min/Max '.$data['minMs'].'/'.$data['maxMs'].' ms'; } if (isset($data['p95Ms']) && $data['p95Ms'] !== null) { $sum[] = 'p95 '.$data['p95Ms'].' ms'; } if (isset($data['laengsterAusfallSek']) && $data['laengsterAusfallSek'] !== null) { $sum[] = 'längster Ausfall '.$data['laengsterAusfallSek'].' s'; } $out = '
'.dol_escape_htmltag(implode(' | ', $sum)).'
'; $ausfaelle = (isset($data['ausfaelle']) && is_array($data['ausfaelle'])) ? $data['ausfaelle'] : array(); if (empty($ausfaelle)) { $out .= '
Kein Ausfall während des Laufs.
'; } else { $out .= '
Ausfälle ('.count($ausfaelle).'):
    '; foreach ($ausfaelle as $line) { $out .= '
  • '.dol_escape_htmltag((string) $line).'
  • '; } $out .= '
'; } $out .= '
'; return $out; } /** * WLAN-Kanal-Momentaufnahme strukturiert als HTML aufbereiten. * * Wie beim Dauertest ein eigener Zweig statt der generischen Darstellung: * `netze` ist eine Liste von Objekten und würde dort als JSON-Text mit * dol_trunc(…,200) mitten im Satz abgeschnitten. * * @param array $data bereits dekodiertes result-JSON * @return string HTML-Schnipsel */ function netdiagFormatWifiKanal($data) { $out = '
'; // Aus dem Demomodus der App: erfundene Netze zum Ansehen/Vorführen. Muss // deutlich sichtbar bleiben, sonst wird eine Demo später für eine echte // Messung am Kundenstandort gehalten. if (!empty($data['demodaten'])) { $out .= '
' .dol_escape_htmltag('DEMODATEN — keine echte Messung').'
'; } $sum = array(); if (isset($data['anzahlNetze'])) { $sum[] = $data['anzahlNetze'].' Netze sichtbar'; } if (!empty($data['eigenesNetz']) && is_array($data['eigenesNetz'])) { $e = $data['eigenesNetz']; $teil = 'Eigenes Netz: '.($e['ssid'] ?? '?').' — Kanal '.($e['kanal'] ?? '?'); if (!empty($e['band'])) { $teil .= ' ('.$e['band'].')'; } if (isset($e['rssi'])) { $teil .= ', '.$e['rssi'].' dBm'; } if (!empty($e['bewertung'])) { $teil .= ' — '.$e['bewertung']; } $sum[] = $teil; } if (!empty($data['empfehlung24GHz']) && is_array($data['empfehlung24GHz'])) { $best = reset($data['empfehlung24GHz']); if (is_array($best) && isset($best['kanal'])) { $sum[] = 'Störungsärmster 2,4-GHz-Kanal: '.$best['kanal']; } } if (!empty($sum)) { $out .= '
'.dol_escape_htmltag(implode(' | ', $sum)).'
'; } if (!empty($data['warnungen']) && is_array($data['warnungen'])) { $out .= '
Hinweise:
    '; foreach ($data['warnungen'] as $w) { $out .= '
  • '.dol_escape_htmltag((string) $w).'
  • '; } $out .= '
'; } $netze = (isset($data['netze']) && is_array($data['netze'])) ? $data['netze'] : array(); if (!empty($netze)) { $out .= ''; $out .= ''; $out .= ''; $out .= ''; foreach ($netze as $n) { if (!is_array($n)) { continue; } $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; } $out .= '
SSIDKanalBandBreiteStandardSicherheitPegel
'.dol_escape_htmltag((string) ($n['ssid'] ?? '')).''.dol_escape_htmltag((string) ($n['kanal'] ?? '')).''.dol_escape_htmltag((string) ($n['band'] ?? '')).''.(isset($n['breiteMhz']) && $n['breiteMhz'] !== null ? dol_escape_htmltag($n['breiteMhz'].' MHz') : '-').''.dol_escape_htmltag((string) ($n['standard'] ?? '-')).''.dol_escape_htmltag((string) ($n['sicherheit'] ?? '-')).''.dol_escape_htmltag((string) ($n['rssi'] ?? '')).' dBm
'; } $out .= '
'; return $out; } /** * Klartext-Bezeichnung und Einheit für einen Ergebnis-Schlüssel. * * Vorher wurde der rohe JSON-Schlüssel nur großgeschrieben ausgegeben — das * Kundendokument las sich wie ein Datenbank-Auszug: „VerlustProzent: 0 | * MinMs: 4.4 | ArpAvailable: nein | MdnsOk: ja". Kennzahlen standen ohne * Einheit da („UptimeSek: 8123456"), und interne Messwerkzeug-Details, die den * Kunden nichts angehen, standen mitten im Dokument. * * Bewusst eine WHITELIST: was hier nicht steht, erscheint nicht im * Kundendokument. Ein neues Werkzeug muss also einmalig hier eingetragen * werden — dafür kann nie versehentlich ein internes Feld durchrutschen. * Die Technikeransicht in Dolibarr zeigt weiterhin alle Felder. * * @return array Schlüssel => [Bezeichnung, Einheit] */ function netdiagKundenfelder() { return array( // gemeinsam 'host' => array('Ziel', ''), 'subnet' => array('Netzbereich', ''), 'count' => array('Gefundene Geräte', ''), // Ping / Laufzeit 'gesendet' => array('Gesendet', 'Pakete'), 'empfangen' => array('Empfangen', 'Pakete'), 'verlustProzent' => array('Paketverlust', '%'), 'minMs' => array('Kürzeste Antwortzeit', 'ms'), 'avgMs' => array('Mittlere Antwortzeit', 'ms'), 'medianMs' => array('Typische Antwortzeit', 'ms'), 'maxMs' => array('Längste Antwortzeit', 'ms'), 'p95Ms' => array('Antwortzeit (95 %)', 'ms'), 'jitterMs' => array('Schwankung', 'ms'), 'verfahren' => array('Messverfahren', ''), // Durchsatz 'downMbps' => array('Download', 'Mbit/s'), 'upMbps' => array('Upload', 'Mbit/s'), 'mbitProSekunde' => array('Durchsatz', 'Mbit/s'), // Dauertest 'dauerSekunden' => array('Messdauer', 's'), 'intervallSek' => array('Messabstand', '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)', '%'), // „Ausfallzeit" allein war irreführend: der Wert ist der des schlechtesten // Geräts, nicht die Summe über alle. Bei zehn überwachten Geräten liest // sich „Ausfallzeit: 8 min" sonst wie eine Gesamtaussage. 'ausfallzeitSek' => array('Ausfallzeit (schlechtestes Gerät)', 's'), 'aussetzer' => array('Aussetzer', ''), 'geraete' => array('Je Gerät', ''), 'ausfaelle' => array('Einzelne Ausfälle', ''), // Internet-Kette (Anschluss → Gateway → Namensauflösung → HTTPS). // 'fehlerglied' ist die eigentliche Aussage des Werkzeugs — ohne diesen // Eintrag stünde im Kundendokument nur die Ampel. 'fehlerglied' => array('Fehlerglied', ''), 'stufen' => array('Geprüfte Stufen', ''), 'systemUrteil' => array('Urteil des Betriebssystems', ''), // Kommen aus dem LinkInfo-Objekt und damit auch im IP-Test (Dose) vor 'validated' => array('Internetverbindung bestätigt', ''), 'captivePortal' => array('Anmeldeseite erkannt', ''), // DNS-Prüfung: je ein Paket an je einen Server. `antworten` ist die // eigentliche Aussage — welcher Server antwortet und welcher nicht. 'gefragteServer' => array('Befragte DNS-Server', ''), 'antworten' => array('Antworten der Server', ''), // Netz/DHCP 'server' => array('DHCP-Server', ''), 'lease' => array('Lease-Dauer', 's'), 'gateway' => array('Gateway', ''), 'dns' => array('DNS-Server', ''), // SNMP / Switch 'sysDescr' => array('Gerätebeschreibung', ''), 'uptimeSek' => array('Betriebszeit', 's'), // Traceroute 'reachedTarget' => array('Ziel erreicht', ''), // WLAN 'anzahlNetze' => array('Sichtbare WLAN-Netze', ''), // --- Schlüssel, welche die App TATSÄCHLICH liefert --- // Sie weichen teils von den obigen ab (deutsche Namen, andere Schreibung). // Ohne diese Einträge verwirft die Whitelist sie stillschweigend und im // Kundendokument steht bei Portscan, Traceroute, iPerf, SNMP und // IP-Konflikt nur noch die Ampel — ohne einen einzigen Messwert. Genau // das ist beim ersten Bau dieser Tabelle passiert, weil sie nur gegen // selbst erzeugte Testdaten geprüft wurde statt gegen die echten // Werkzeuge. Beim Anlegen eines Werkzeugs hier eintragen. 'ziel' => array('Ziel', ''), 'zielErreicht' => array('Ziel erreicht', ''), 'hops' => array('Zwischenstellen', ''), 'gegenstelle' => array('Gegenstelle', ''), 'downloadMbps' => array('Download', 'Mbit/s'), 'uploadMbps' => array('Upload', 'Mbit/s'), 'ip' => array('Gerät', ''), 'scanned' => array('Geprüfte Ports', ''), 'open' => array('Offene Ports', ''), 'geprueft' => array('Geprüfte Adressen', ''), 'runden' => array('Messrunden', ''), 'konflikte' => array('IP-Konflikte', ''), 'port' => array('Switch-Port', ''), 'linkSpeed' => array('Verbindungsgeschwindigkeit', ''), 'eingangsFehler' => array('Fehler eingehend', ''), 'ausgangsFehler' => array('Fehler ausgehend', ''), // IP-Test (Dose): die Seite reicht das LinkInfo-Objekt durch 'ipv4' => array('IP-Adresse', ''), 'prefixLength' => array('Präfixlänge', ''), 'dhcpServer' => array('DHCP-Server', ''), // 'system' = vom Betriebssystem gemeldet (gilt für jeden Anschlusstyp), // 'wlan' = alter WLAN-Weg. Macht einen fehlenden Wert einordenbar. 'dhcpQuelle' => array('Quelle der DHCP-Angabe', ''), 'leaseSec' => array('Lease-Dauer', 's'), 'linkSpeedMbps' => array('Verbindungsgeschwindigkeit', 'Mbit/s'), 'rxLinkSpeedMbps' => array('Empfangsgeschwindigkeit', 'Mbit/s'), 'ssid' => array('WLAN-Name', ''), 'raum' => array('Raum / Dose', ''), 'iface' => array('Schnittstelle', ''), // 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', ''), 'nichtMehrErreichbar' => array('Nicht mehr erreichbar', ''), '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 // im Kundendokument ohne jede Erklärung stehen. Der Text läuft durch // netdiagKundentext() und wird dort von Messgeräte-Interna befreit. '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', ''), 'kundenhinweis' => array('Hinweis', ''), ); } /** * Bekannte Falschaussagen aus Altmessungen bei der Anzeige geraderücken. * * Betrifft das entfernte Werkzeug `dhcpcheck`. In der Produktionsdatenbank * stehen drei ausgelieferte Messungen mit dem Label „0 DHCP-Server (!)", * Ergebnis `{"count":0,"server":[],"hinweis":""}` und **Status 2 (rot)** — eine * Aussage über das Kundennetz, welche die Messung nie hergab: bei einer * Kabelverbindung gibt Android die DHCP-Lease grundsätzlich nicht heraus (sie * steckt nur im deprecated `DhcpInfo` und nur für WLAN). Gemessen wurde also * nichts; „0 gefunden" ist der Rückgabewert für „nicht ermittelbar". * * Korrigiert wird bei der **Anzeige**, nicht in der Datenbank: die gespeicherte * Messung bleibt unangetastet (Rohdaten sind Beleg), aber jedes neu erzeugte * Kundendokument zeigt statt der roten Ampel ein ehrliches „nicht messbar". * * @param string $tool Werkzeug-ID der Messung * @param int $status gespeicherter Status * @param string $label gespeichertes Label * @param string $json gespeichertes Ergebnis-JSON * @return array{status:int,label:string,hinweis:string} korrigierte Anzeige */ function netdiagAltlastKorrektur($tool, $status, $label, $json) { $out = array('status' => (int) $status, 'label' => (string) $label, 'hinweis' => ''); if ($tool !== 'dhcpcheck') { return $out; } $data = json_decode((string) $json, true); if (!is_array($data)) { return $out; } $leer = (isset($data['count']) && (int) $data['count'] === 0) && (!isset($data['server']) || $data['server'] === array() || $data['server'] === ''); if (!$leer) { return $out; } $out['status'] = 3; // nicht messbar $out['label'] = 'DHCP-Server nicht ermittelbar'; $out['hinweis'] = 'Diese Messung stammt aus einer früheren App-Fassung. Bei einer ' .'Kabelverbindung gibt Android die DHCP-Angaben nicht heraus — es wurde also nichts ' .'gemessen. Die frühere Darstellung („0 DHCP-Server") las sich wie ein Befund über ' .'das Kundennetz und war keiner.'; return $out; } /** * Sekundenwert lesbar machen (aus „8123456" wird „94 Tage 0 Std"). * * @param int|float $sek Sekunden * @return string lesbare Dauer */ function netdiagDauerLesbar($sek) { $sek = (int) $sek; if ($sek < 60) { return $sek.' s'; } if ($sek < 3600) { return round($sek / 60).' min'; } if ($sek < 86400) { $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' : ''); } // (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' : ''); } /** * Ein Ergebnisfeld für das Kundendokument aufbereiten. * * @param string $key Schlüssel aus dem Ergebnis-JSON * @param mixed $val Wert * @return string|null „Bezeichnung: Wert Einheit" oder null, wenn das Feld * nicht ins Kundendokument gehört */ function netdiagFeldFuerKunde($key, $val) { $felder = netdiagKundenfelder(); if (!isset($felder[$key])) { return null; // internes Feld — bewusst nicht im Kundendokument } list($label, $einheit) = $felder[$key]; if (is_array($val)) { $flat = array(); foreach ($val as $item) { $flat[] = is_array($item) ? json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $item; } $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)) { $val = $val ? 'ja' : 'nein'; } elseif ($val === null || $val === '') { return null; // nicht ermittelt — lieber weglassen als „: " drucken } /* * Adressfelder mit dem Wert 0 bzw. 0.0.0.0 sind KEINE Messung, sondern der * Rückgabewert „nichts ermittelt" der alten Android-API. In ausgelieferten * Protokollen steht deshalb „DHCP-Server: 0" — eine Angabe, die es so im * Kundennetz nie gab, und bei einer Kabelverbindung schlicht der Umstand, * dass Android die Lease dort gar nicht herausgibt (nur WLAN, deprecated). * Rückwirkend wirksam: Protokoll-PDFs werden bei jedem Abruf neu erzeugt. * Lease-Dauer 0 fällt in dieselbe Kategorie. */ $adressfelder = array('server', 'dhcpServer', 'gateway', 'dns', 'ipv4', 'ip'); if (in_array($key, $adressfelder, true) && ($val === 0 || $val === '0' || $val === '0.0.0.0')) { return $label.': nicht ermittelbar'; } if (in_array($key, array('lease', 'leaseSec'), true) && ($val === 0 || $val === '0')) { return $label.': nicht ermittelbar'; } // Sekundenwerte lesbar machen statt sechsstellige Zahlen zu drucken if ($einheit === 's' && is_numeric($val) && $val >= 3600) { return $label.': '.netdiagDauerLesbar($val); } $text = netdiagKundentext((string) $val); /* * Einheit nicht doppelt anhängen. Ältere App-Fassungen haben sie in den * Wert geschrieben („864000 s"); im Kundendokument stand dadurch * „Lease-Dauer: 864000 s s". Nachgewiesen an der Prod-Messung #42. */ if ($einheit !== '' && preg_match('/\s'.preg_quote($einheit, '/').'$/u', $text)) { return $label.': '.$text; } return $label.': '.$text.($einheit !== '' ? ' '.$einheit : ''); } /** * Interne Formulierungen aus Alt-Datensätzen für das Kundendokument * entschärfen. * * Beispiel aus einem echten Protokoll: „ARP-Tabelle nicht lesbar * (/proc/net/arp) — braucht Root". Das ist eine Aussage über das Messgerät, * nicht über das Kundennetz, und gehört nicht ins Abnahmedokument. Neue * Messungen liefern gleich einen Kundentext; für die bereits gespeicherten * Ergebnisse hilft nur ein Ersetzen beim Drucken. * * @param string $text Rohtext aus dem Ergebnis * @return string kundentauglicher Text */ function netdiagKundentext($text) { 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.'; } // 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; } /** * Klarname eines Werkzeugs für die Anzeige. * * Die kanonischen Namen stehen eigentlich in der App (`Tool.name`), das * Dolibarr-Modul kann darauf aber nicht zugreifen. Deshalb hier eine bewusst * kurze Zuordnung statt eines zweiten Satzes Sprachschlüssel — mit Rückfall * auf die ID, damit ein neues Werkzeug nie einen leeren Titel bekommt. * * Enthält auch Werkzeuge, die es in der App nicht mehr gibt („dhcpcheck", * „wifiscan"): in der Produktionsdatenbank stehen dazu noch Messungen aus * älteren Protokollen, und die sollen weiterhin lesbar sein. * * @param string $tool Werkzeug-ID aus der Messung * @return string Klarname, oder die ID selbst wenn unbekannt */ function netdiagToolName($tool) { $map = array( 'ipscan' => 'IP-Scanner', 'portscan' => 'Port-Scan', 'ping' => 'Ping / Qualität', 'ipconflict' => 'IP-Konflikt', 'snmp' => 'SNMP-Switch', 'traceroute' => 'Traceroute', 'stresstest' => 'Dauer-/Stresstest', 'iperf' => 'Durchsatz-Test', 'internetkette' => 'Internet-Kette', 'wifikanal' => 'WLAN-Kanalanalyse', 'monitor' => 'Geräte-Monitor', 'iptest' => 'IP-Test (Dose)', 'wifitrack' => 'WLAN-Empfang', 'dnscheck' => 'DNS-Prüfung', // nicht mehr vorhandene Werkzeuge, aber in Altprotokollen enthalten 'dhcpcheck' => 'DHCP-Prüfung', 'wifiscan' => 'WLAN-Scan', ); return $map[$tool] ?? $tool; } /** * Messparameter für die Anzeige aufbereiten („Ziel: 192.168.1.1"). * * Ohne sie steht im Protokoll zwar das Ergebnis, aber nicht, worauf es sich * bezieht — bei Prod-Messung #126 war deshalb nicht erkennbar, gegen welches * Ziel der Dauertest lief. Nur ausgefüllte Parameter erscheinen. * * @param string $json JSON-String der Parameter * @return string lesbarer Text, leer wenn nichts anzuzeigen ist */ function netdiagFormatParams($json) { if (empty($json)) { return ''; } $data = json_decode($json, true); if (!is_array($data) || empty($data)) { return ''; } $labels = array( 'host' => 'Ziel', 'subnet' => 'Netzbereich', 'ip' => 'Gerät', 'ports' => 'Ports', 'port' => 'Port', 'count' => 'Anzahl Proben', 'durationSec' => 'Dauer (s)', 'intervalSec' => 'Messabstand (s)', 'community' => 'SNMP-Community', 'rounds' => 'Runden', 'timeoutMs' => 'Zeitgrenze (ms)', ); $teile = array(); foreach ($data as $k => $v) { if ($v === null || $v === '' || is_array($v)) { continue; } $teile[] = ($labels[$k] ?? ucfirst((string) $k)).': '.$v; } return implode(' · ', $teile); }