dolibarr.netdiag/lib/netdiag.lib.php
Eduard Wisch 90cbee6e81 Geraete-Monitor: eigener PDF-Zweig statt einer Sammelzelle
Die Dauerueberwachung lief im generischen else-Zweig und wurde von
netdiagPdfFlattenResult() zu EINER |-getrennten Zeile zusammengeschoben.
Bei zehn ueberwachten Geraeten standen damit die zehn Geraete-Zeilen und
saemtliche Einzelausfaelle in einer einzigen Tabellenzelle - vollstaendig,
aber beim Kunden nicht lesbar. Dauertest und WLAN-Kanal hatten laengst je
einen Tabellen-Renderer, der Monitor nicht.

netdiagPdfMonitor() gibt jetzt aus: Kennzahlenzeile, Tabelle "Ueberwachte
Geraete", Tabelle "Einzelne Ausfaelle" mit Uhrzeiten, Hinweis zum
Messverfahren am Schluss. Tabellenkoepfe wiederholen sich nach einem
Seitenumbruch.

Die Falle dabei, gegen die im Code ein Kommentar steht: beim Dauertest
fuehrt ein gesetztes 'hinweis' zum sofortigen Abbruch der Ausgabe. Der
Monitor legt aber IMMER einen Hinweis an (das Messverfahren gehoert zur
Aussage dazu) - derselbe Aufbau haette hier bei jedem Lauf alle Zahlen
verschluckt. Nur 'fehler' bricht ab.

Obergrenze 200 Ausfallzeilen gegen ein im Sekundentakt flappendes Geraet.
Gekuerzt wird sichtbar ("... und N weitere Ausfaelle") - stilles Kuerzen
waere bei einem Abnahmebeleg eine Falschaussage.

Ausserdem: 'ausfallzeitSek' heisst jetzt "Ausfallzeit (schlechtestes
Geraet)". Der Wert ist der des schlechtesten Geraets, nicht die Summe -
bei zehn Geraeten las sich "Ausfallzeit: 8 min" wie eine Gesamtaussage.

Geprueft gegen die lokale Testinstanz mit vier konstruierten Faellen
(10 Geraete mit Aussetzern, ein durchgehend totes Geraet, 250 Ausfaelle
ueber sechs Seiten, abgerissener Lauf) und gegen die echte Monitor-
Messung aus ND2026-0016. PDF gerendert und angesehen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:22:02 +02:00

614 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'),
// 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', ''),
// 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);
}