dolibarr.netdiag/lib/netdiag.lib.php
Eduard Wisch 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

601 lines
22 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* 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 <https://www.gnu.org/licenses/>.
*/
/**
* \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<int,array<int,string>> 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<int,array<int,string>> 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 '<span class="opacitymedium">-</span>';
}
$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 = '<div class="netdiag-result">';
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 .= '<span class="netdiag-kv"><strong>'.$label.':</strong> '.dol_escape_htmltag(dol_trunc($valstr, 200)).'</span> ';
}
$out .= '</div>';
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<string,mixed> $data bereits dekodiertes result-JSON
* @return string HTML-Schnipsel
*/
function netdiagFormatStressTest($data)
{
if (!empty($data['hinweis']) || !empty($data['fehler'])) {
return '<span class="opacitymedium">'.dol_escape_htmltag((string) ($data['hinweis'] ?? $data['fehler'])).'</span>';
}
$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 = '<div class="netdiag-result"><div class="netdiag-kv">'.dol_escape_htmltag(implode(' | ', $sum)).'</div>';
$ausfaelle = (isset($data['ausfaelle']) && is_array($data['ausfaelle'])) ? $data['ausfaelle'] : array();
if (empty($ausfaelle)) {
$out .= '<div class="opacitymedium">Kein Ausfall während des Laufs.</div>';
} else {
$out .= '<div><strong>Ausfälle ('.count($ausfaelle).'):</strong></div><ul class="netdiag-outages">';
foreach ($ausfaelle as $line) {
$out .= '<li>'.dol_escape_htmltag((string) $line).'</li>';
}
$out .= '</ul>';
}
$out .= '</div>';
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<string,mixed> $data bereits dekodiertes result-JSON
* @return string HTML-Schnipsel
*/
function netdiagFormatWifiKanal($data)
{
$out = '<div class="netdiag-result">';
// 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 .= '<div class="netdiag-kv"><strong style="color:#b45309">'
.dol_escape_htmltag('DEMODATEN — keine echte Messung').'</strong></div>';
}
$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 .= '<div class="netdiag-kv">'.dol_escape_htmltag(implode(' | ', $sum)).'</div>';
}
if (!empty($data['warnungen']) && is_array($data['warnungen'])) {
$out .= '<div><strong>Hinweise:</strong></div><ul class="netdiag-outages">';
foreach ($data['warnungen'] as $w) {
$out .= '<li>'.dol_escape_htmltag((string) $w).'</li>';
}
$out .= '</ul>';
}
$netze = (isset($data['netze']) && is_array($data['netze'])) ? $data['netze'] : array();
if (!empty($netze)) {
$out .= '<table class="noborder centpercent"><tr class="liste_titre">';
$out .= '<th>SSID</th><th class="center">Kanal</th><th>Band</th>';
$out .= '<th class="center">Breite</th><th>Standard</th><th>Sicherheit</th><th class="right">Pegel</th>';
$out .= '</tr>';
foreach ($netze as $n) {
if (!is_array($n)) {
continue;
}
$out .= '<tr class="oddeven">';
$out .= '<td>'.dol_escape_htmltag((string) ($n['ssid'] ?? '')).'</td>';
$out .= '<td class="center">'.dol_escape_htmltag((string) ($n['kanal'] ?? '')).'</td>';
$out .= '<td>'.dol_escape_htmltag((string) ($n['band'] ?? '')).'</td>';
$out .= '<td class="center">'.(isset($n['breiteMhz']) && $n['breiteMhz'] !== null ? dol_escape_htmltag($n['breiteMhz'].' MHz') : '-').'</td>';
$out .= '<td>'.dol_escape_htmltag((string) ($n['standard'] ?? '-')).'</td>';
$out .= '<td>'.dol_escape_htmltag((string) ($n['sicherheit'] ?? '-')).'</td>';
$out .= '<td class="right">'.dol_escape_htmltag((string) ($n['rssi'] ?? '')).' dBm</td>';
$out .= '</tr>';
}
$out .= '</table>';
}
$out .= '</div>';
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<string,array{0:string,1:string}> 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'),
// 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', ''),
'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', ''),
);
}
/**
* 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
}
// Sekundenwerte lesbar machen statt sechsstellige Zahlen zu drucken
if ($einheit === 's' && is_numeric($val) && $val >= 3600) {
return $label.': '.netdiagDauerLesbar($val);
}
$text = netdiagKundentext((string) $val);
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',
'wifikanal' => 'WLAN-Kanalanalyse',
'monitor' => 'Geräte-Monitor',
'iptest' => 'IP-Test (Dose)',
'wifitrack' => 'WLAN-Empfang',
// 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);
}