From 0b3db47ec10dd9cfacf67ff6b706ca5a81230dcc Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Sat, 15 Aug 2026 18:54:01 +0200 Subject: [PATCH] =?UTF-8?q?Phase=205:=20Kundendokument=20lesbar,=20Ger?= =?UTF-8?q?=C3=A4temerkmale=20kommen=20endlich=20an?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorab: drei Punkte der Roadmap-Liste waren Fehlannahmen. Eine Analyse mit anschließender Gegenprüfung (jeder Befund musste einen Widerlegungsversuch überstehen) hat sie ausgeräumt, bevor Code geändert wurde: - "ab Seite 2 alles nach rechts verschoben" existiert nicht. Nachgemessen am Prod-PDF ND2026-0015 mit pdftotext -bbox: Seite 1 und Seite 2 beginnen beide bei 16,0 mm. Die echten Umbruchfehler waren andere. - measure_status validieren war seit Phase 1 erledigt. - Werkzeug-IDs / Teilnetz-Gruppierung / TCPDF-Fußzeile: verworfen, die vorgeschlagenen Änderungen hätten das PDF verschlechtert. PDF (alle Punkte am mehrseitigen Dokument nachgeprüft): - Tabellenkopf der Geräteliste wird auf Folgeseiten wiederholt. Vorher standen ab Seite 2 unbeschriftete Spalten — bei leeren MAC/Hostname-Feldern vier namenlose Spalten. - Messungs-Titelzeile und Ergebnis werden zusammengehalten. Vorher blieb die Überschrift samt Ampel am Seitenende allein zurück, darunter ein leerer, unten offener Rahmen; in einem Testlauf über 61 Umbruchlagen 5-mal (~8 %). - Spalte "Gerätetyp" hatte 15 mm, ließ aber 10 Zeichen zu — "Chromecast/TV" lief bis 199,0 mm bei 195 mm Tabellenkante über den Rahmen in den Druckrand. - Deutsche Bezeichnungen mit Einheiten statt roher JSON-Schlüssel: aus "VerlustProzent: 0 | MinMs: 4.4 | UptimeSek: 8123456" wird "Paketverlust: 0 % | Kürzeste Antwortzeit: 4.4 ms | Betriebszeit: 94 Tage 1 Std". Als Whitelist (netdiagKundenfelder(), gemeinsam für Karte und PDF) — interne Felder wie arpAvailable, mdnsOk, probed, answered fallen damit automatisch heraus. - "ARP-Tabelle nicht lesbar (/proc/net/arp) — braucht Root" wird beim Drucken zu einem kundentauglichen Satz. Altdaten stehen so in der DB, deshalb Ersetzung beim Drucken statt nur in der App. Gerätemerkmale (der eigentliche Roadmap-Punkt): Der Techniker sah in der App "Drucker HP, Port 9100", im Kundenprotokoll stand nur die IP. Die Felder fehlten dabei nicht in der Übertragung, sondern durchgängig — ein Fix allein in der API wäre folgenlos geblieben, weil Dolibarrs setSaveQuery() nur deklarierte $fields schreibt. Ergänzt über die ganze Kette: sql/llx_netdiag_device.sql + neue Migration llx_netdiag_device_v2.sql (ADD COLUMN IF NOT EXISTS, wiederholbar, läuft bei jedem Modul-Update), NetDiagDevice::$fields + Properties, api/protocols.php POST und GET, Kartenansicht und PDF. Neu: netbios_name, mdns_name, mdns_services, custom_name, open_ports, found_via, last_seen. Im PDF steht jetzt statt "192.168.178.20" die Zeile "Brother HL-L2350DW · Brother · Drucker · 80,443,9100" — der Name kommt aus mDNS, obwohl der Hostname leer ist. Sprachschlüssel Vendor -> NetDiagVendor: Die Gegenprüfung hielt den Punkt für falsch (Translate::load() ist first-wins, im CLI-Test kam "Hersteller"), im Browser stand in der Kartenansicht aber "Lieferant" — im HTTP-Kontext lädt Dolibarr vorher andere Sprachdateien als im CLI. Statt der Ursache nachzugehen jetzt ein eigener, kollisionsfreier Schlüssel; im Browser gegengeprüft. Nebenbei: doppeltes "OK OK" beim Status 0 im PDF. Gegen das Test-Dolibarr geprüft: Sync über die echte API (Login, POST, GET), Felder in der DB kontrolliert, Kartenansicht im Browser, mehrseitiges PDF gerendert und angesehen, Migration zweimal ausgeführt (idempotent). --- api/netdiag_api.lib.php | 52 +++++++++++++ api/protocols.php | 31 ++++++-- class/netdiagdevice.class.php | 21 +++++ langs/de_DE/netdiag.lang | 14 ++++ langs/en_US/netdiag.lang | 14 ++++ lib/netdiag.lib.php | 141 ++++++++++++++++++++++++++++++++++ lib/netdiag_pdf.lib.php | 121 ++++++++++++++++++++++------- netdiagprotocol_card.php | 36 ++++++++- sql/llx_netdiag_device.sql | 13 ++++ sql/llx_netdiag_device_v2.sql | 26 +++++++ 10 files changed, 432 insertions(+), 37 deletions(-) create mode 100644 sql/llx_netdiag_device_v2.sql diff --git a/api/netdiag_api.lib.php b/api/netdiag_api.lib.php index a801750..bc0f8ed 100644 --- a/api/netdiag_api.lib.php +++ b/api/netdiag_api.lib.php @@ -438,3 +438,55 @@ function netdiag_api_protocol_list($db, $filtersql = '') } return $list; } + +/** + * Liste (Array aus der App) für die Datenbank zu einer Komma-Zeichenkette + * zusammenfassen. + * + * Ports und mDNS-Dienste sind in der App Arrays; in der Tabelle liegen sie als + * Textspalte. Bewusst mit Längenbegrenzung: ein Gerät mit sehr vielen Diensten + * darf den INSERT nicht scheitern lassen (MariaDB kürzt im strict mode nicht, + * sondern lehnt ab). + * + * @param mixed $val Array oder Skalar aus dem JSON-Payload + * @param int $maxLen Maximale Länge der Zielspalte + * @return string Komma-Liste, ggf. gekürzt + */ +function netdiag_join_list($val, $maxLen) +{ + if ($val === null || $val === '') { + return ''; + } + if (!is_array($val)) { + $val = array($val); + } + $teile = array(); + foreach ($val as $v) { + if (is_array($v)) { + continue; // verschachtelte Strukturen gehören nicht in eine Textspalte + } + $s = trim((string) $v); + if ($s !== '') { + $teile[] = str_replace(',', ' ', $s); + } + } + $out = implode(',', $teile); + return strlen($out) > $maxLen ? substr($out, 0, $maxLen) : $out; +} + +/** + * Komma-Zeichenkette aus der Datenbank wieder als Liste liefern. + * + * @param string|null $val Spaltenwert + * @return array + */ +function netdiag_split_list($val) +{ + if ($val === null || trim((string) $val) === '') { + return array(); + } + $teile = array_map('trim', explode(',', (string) $val)); + return array_values(array_filter($teile, static function ($t) { + return $t !== ''; + })); +} diff --git a/api/protocols.php b/api/protocols.php index 452ae13..6e4da79 100644 --- a/api/protocols.php +++ b/api/protocols.php @@ -55,13 +55,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { $devices = array(); foreach ($devObj->fetchAllByProtocol($protocol->id) as $d) { $devices[] = array( - 'id' => (int) $d->id, - 'ip' => $d->ip, - 'mac' => $d->mac, - 'hostname' => $d->hostname, - 'vendor' => $d->vendor, - 'deviceType' => $d->devicetype, - 'note' => $d->note, + 'id' => (int) $d->id, + 'ip' => $d->ip, + 'mac' => $d->mac, + 'hostname' => $d->hostname, + 'netbiosName' => $d->netbios_name, + 'mdnsName' => $d->mdns_name, + 'mdnsServices' => netdiag_split_list($d->mdns_services), + 'customName' => $d->custom_name, + 'vendor' => $d->vendor, + 'deviceType' => $d->devicetype, + 'openPorts' => array_map('intval', netdiag_split_list($d->open_ports)), + 'foundVia' => $d->found_via, + 'lastSeen' => !empty($d->last_seen) ? (int) $db->jdate($d->last_seen) * 1000 : null, + 'note' => $d->note, ); } $measObj = new NetDiagMeasurement($db); @@ -171,6 +178,16 @@ foreach ($devicesIn as $d) { $dev->vendor = isset($d['vendor']) ? (string) $d['vendor'] : ''; $dev->devicetype = isset($d['deviceType']) ? (string) $d['deviceType'] : ''; $dev->note = isset($d['note']) ? (string) $d['note'] : ''; + // Gerätemerkmale aus dem IP-Scan. Ohne sie steht im Kundenprotokoll nur + // die IP, während der Techniker in der App „Drucker HP, Port 9100" sieht. + $dev->netbios_name = isset($d['netbiosName']) ? (string) $d['netbiosName'] : ''; + $dev->mdns_name = isset($d['mdnsName']) ? (string) $d['mdnsName'] : ''; + $dev->mdns_services = netdiag_join_list($d['mdnsServices'] ?? null, 512); + $dev->custom_name = isset($d['customName']) ? (string) $d['customName'] : ''; + $dev->open_ports = netdiag_join_list($d['openPorts'] ?? null, 255); + $dev->found_via = isset($d['foundVia']) ? substr((string) $d['foundVia'], 0, 16) : ''; + // lastSeen kommt als Unix-Zeit in Millisekunden aus der App + $dev->last_seen = !empty($d['lastSeen']) ? $db->idate((int) round(((float) $d['lastSeen']) / 1000)) : null; $dev->tms = dol_now(); if ($dev->create($user, 1) <= 0) { $db->rollback(); diff --git a/class/netdiagdevice.class.php b/class/netdiagdevice.class.php index a0b0c73..5aefba5 100644 --- a/class/netdiagdevice.class.php +++ b/class/netdiagdevice.class.php @@ -53,8 +53,15 @@ class NetDiagDevice extends CommonObject 'ip' => array('type' => 'varchar(45)', 'label' => 'IpAddress', 'enabled' => 1, 'visible' => 1, 'position' => 20), 'mac' => array('type' => 'varchar(17)', 'label' => 'MacAddress', 'enabled' => 1, 'visible' => 1, 'position' => 25), 'hostname' => array('type' => 'varchar(255)', 'label' => 'Hostname', 'enabled' => 1, 'visible' => 1, 'position' => 30), + 'netbios_name' => array('type' => 'varchar(64)', 'label' => 'NetDiagNetbiosName', 'enabled' => 1, 'visible' => 1, 'position' => 31), + 'mdns_name' => array('type' => 'varchar(255)', 'label' => 'NetDiagMdnsName', 'enabled' => 1, 'visible' => 1, 'position' => 32), + 'mdns_services' => array('type' => 'varchar(512)', 'label' => 'NetDiagMdnsServices', 'enabled' => 1, 'visible' => 3, 'position' => 33), + 'custom_name' => array('type' => 'varchar(255)', 'label' => 'NetDiagCustomName', 'enabled' => 1, 'visible' => 1, 'position' => 34), 'vendor' => array('type' => 'varchar(128)', 'label' => 'Vendor', 'enabled' => 1, 'visible' => 1, 'position' => 35), 'devicetype' => array('type' => 'varchar(64)', 'label' => 'DeviceType', 'enabled' => 1, 'visible' => 1, 'position' => 40), + 'open_ports' => array('type' => 'varchar(255)', 'label' => 'NetDiagOpenPorts', 'enabled' => 1, 'visible' => 1, 'position' => 41), + 'found_via' => array('type' => 'varchar(16)', 'label' => 'NetDiagFoundVia', 'enabled' => 1, 'visible' => 1, 'position' => 42), + 'last_seen' => array('type' => 'datetime', 'label' => 'NetDiagLastSeen', 'enabled' => 1, 'visible' => 1, 'position' => 43), 'note' => array('type' => 'text', 'label' => 'Note', 'enabled' => 1, 'visible' => 3, 'position' => 50), 'date_creation' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'position' => 500), 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => 0, 'notnull' => 0, 'position' => 501), @@ -66,8 +73,22 @@ class NetDiagDevice extends CommonObject public $ip; public $mac; public $hostname; + /** @var string NetBIOS-Name (UDP-137-Abfrage) */ + public $netbios_name; + /** @var string mDNS-/Bonjour-Anzeigename */ + public $mdns_name; + /** @var string angebotene mDNS-Dienste, kommagetrennt */ + public $mdns_services; + /** @var string vom Techniker vergebener Name */ + public $custom_name; public $vendor; public $devicetype; + /** @var string offene Ports, kommagetrennt (z.B. "80,443,9100") */ + public $open_ports; + /** @var string Fundweg: ping | port | arp | mdns */ + public $found_via; + /** @var int|string Unix-Zeit/Datum der letzten Sichtung */ + public $last_seen; public $note; public $date_creation; public $tms; diff --git a/langs/de_DE/netdiag.lang b/langs/de_DE/netdiag.lang index 944d357..5b09fa4 100644 --- a/langs/de_DE/netdiag.lang +++ b/langs/de_DE/netdiag.lang @@ -60,3 +60,17 @@ NetDiagMeasureOk = OK NetDiagMeasureWarn = Warnung NetDiagMeasureFail = Fehler NetDiagMeasureUnmeasurable = Nicht messbar + +# Gerätemerkmale aus dem IP-Scan (seit Phase 5 synchronisiert) +NetDiagVendor=Hersteller +NetDiagNetbiosName=NetBIOS-Name +NetDiagMdnsName=mDNS-Name +NetDiagMdnsServices=mDNS-Dienste +NetDiagCustomName=Eigener Name +NetDiagOpenPorts=Offene Ports +NetDiagFoundVia=Gefunden über +NetDiagFoundViaPing=Ping +NetDiagFoundViaPort=Port +NetDiagFoundViaArp=ARP +NetDiagFoundViaMdns=mDNS +NetDiagLastSeen=Zuletzt gesehen diff --git a/langs/en_US/netdiag.lang b/langs/en_US/netdiag.lang index 120a701..489148e 100644 --- a/langs/en_US/netdiag.lang +++ b/langs/en_US/netdiag.lang @@ -60,3 +60,17 @@ NetDiagMeasureOk = OK NetDiagMeasureWarn = Warning NetDiagMeasureFail = Fail NetDiagMeasureUnmeasurable = Not measurable + +# Device details from the IP scan (synchronised since phase 5) +NetDiagVendor=Manufacturer +NetDiagNetbiosName=NetBIOS name +NetDiagMdnsName=mDNS name +NetDiagMdnsServices=mDNS services +NetDiagCustomName=Custom name +NetDiagOpenPorts=Open ports +NetDiagFoundVia=Found via +NetDiagFoundViaPing=Ping +NetDiagFoundViaPort=Port +NetDiagFoundViaArp=ARP +NetDiagFoundViaMdns=mDNS +NetDiagLastSeen=Last seen diff --git a/lib/netdiag.lib.php b/lib/netdiag.lib.php index 896c565..49b9c9a 100644 --- a/lib/netdiag.lib.php +++ b/lib/netdiag.lib.php @@ -289,3 +289,144 @@ function netdiagFormatWifiKanal($data) $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'), + // 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', ''), + // 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', ''), + '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) { + return floor($sek / 3600).' Std '.round(($sek % 3600) / 60).' min'; + } + return floor($sek / 86400).' Tage '.round(($sek % 86400) / 3600).' 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); + } 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.'; + } + return $text; +} diff --git a/lib/netdiag_pdf.lib.php b/lib/netdiag_pdf.lib.php index d0918f3..a1de688 100644 --- a/lib/netdiag_pdf.lib.php +++ b/lib/netdiag_pdf.lib.php @@ -106,23 +106,38 @@ function netdiagGeneratePdf($db, $protocol, $outputlangs) // Geräteliste $pdf->SetFont('', 'B', 12); $pdf->Cell(0, 7, $outputlangs->transnoentities("NetDiagDevices").' ('.count($devices).')', 0, 1, 'L'); - $pdf->SetFillColor(0, 70, 130); - $pdf->SetTextColor(255, 255, 255); - $pdf->SetFont('', 'B', 8); - $cols = array(35, 40, 50, 40, 15); - $heads = array("IpAddress", "MacAddress", "Hostname", "Vendor", "DeviceType"); - foreach ($heads as $idx => $h) { - $pdf->Cell($cols[$idx], 6, $outputlangs->transnoentities($h), 1, 0, 'L', true); - } - $pdf->Ln(); - $pdf->SetTextColor(0, 0, 0); - $pdf->SetFont('', '', 8); + /* + * Spaltenbreiten (Summe muss 180 mm bleiben, sonst passt die Leerzeile + * unten nicht mehr zum Kopf): Gerätetyp hatte 15 mm, der Text durfte aber + * 10 Zeichen lang sein — „Chromecast/TV" lief dadurch über den rechten + * Tabellenrahmen HINAUS in den Druckrand (nachgemessen an ND2026-0015: + * 199,0 mm bei 195 mm Tabellenkante). Cell() schneidet nicht ab, es + * schreibt einfach weiter. MAC und Hostname sind in der Praxis oft leer + * (ARP ist ab Android 10 gesperrt), von dort kommt der Platz. + */ + $cols = array(30, 34, 44, 28, 24, 20); + $heads = array("IpAddress", "MacAddress", "Hostname", "NetDiagVendor", "DeviceType", "NetDiagOpenPorts"); + netdiagPdfTableHead($pdf, $cols, $heads, $outputlangs); foreach ($devices as $dev) { - $pdf->Cell($cols[0], 5, dol_trunc($dev->ip, 22), 1, 0, 'L'); - $pdf->Cell($cols[1], 5, dol_trunc($dev->mac, 24), 1, 0, 'L'); - $pdf->Cell($cols[2], 5, dol_trunc($dev->hostname, 32), 1, 0, 'L'); - $pdf->Cell($cols[3], 5, dol_trunc($dev->vendor, 26), 1, 0, 'L'); - $pdf->Cell($cols[4], 5, dol_trunc($dev->devicetype, 10), 1, 1, 'L'); + netdiagPdfEnsureSpace($pdf, 5, function ($p) use ($cols, $heads, $outputlangs) { + netdiagPdfTableHead($p, $cols, $heads, $outputlangs); + }); + // Anzeigename wie in der App: eigener Name vor mDNS-/Host-/NetBIOS-Name. + // Ein Drucker ist per DNS oft namenlos, meldet sich per mDNS aber als + // "Brother HL-L2350DW" — im Kundenprotokoll stand bisher nur die IP. + $anzeige = ''; + foreach (array($dev->custom_name, $dev->mdns_name, $dev->hostname, $dev->netbios_name) as $kandidat) { + if (!empty($kandidat)) { + $anzeige = $kandidat; + break; + } + } + $pdf->Cell($cols[0], 5, dol_trunc($dev->ip, 18), 1, 0, 'L'); + $pdf->Cell($cols[1], 5, dol_trunc($dev->mac, 20), 1, 0, 'L'); + $pdf->Cell($cols[2], 5, dol_trunc($anzeige, 26), 1, 0, 'L'); + $pdf->Cell($cols[3], 5, dol_trunc($dev->vendor, 16), 1, 0, 'L'); + $pdf->Cell($cols[4], 5, dol_trunc($dev->devicetype, 14), 1, 0, 'L'); + $pdf->Cell($cols[5], 5, dol_trunc((string) $dev->open_ports, 12), 1, 1, 'L'); } if (empty($devices)) { $pdf->Cell(array_sum($cols), 5, '-', 1, 1, 'C'); @@ -159,6 +174,12 @@ function netdiagGeneratePdf($db, $protocol, $outputlangs) } else { $pdf->SetFillColor(170, 215, 170); } + // Titelzeile und Ergebnis zusammenhalten: sonst steht am Seitenende die + // Überschrift samt Ampel allein und darunter ein leerer, unten offener + // Rahmen — das Ergebnis beginnt erst auf der nächsten Seite. Bei einem + // Testlauf über 61 Umbruchlagen trat das in 5 Fällen auf und liest sich + // wie ein abgebrochenes Dokument. + netdiagPdfEnsureSpace($pdf, 11); $pdf->SetFont('', 'B', 9); $title = ($m->category ? '['.$m->category.'] ' : '').$m->tool.($m->label ? ' — '.$m->label : ''); $pdf->Cell(150, 6, dol_trunc($title, 80), 1, 0, 'L'); @@ -203,22 +224,68 @@ function netdiagPdfFlattenResult($json) } $data = json_decode($json, true); if (!is_array($data)) { - return (string) $json; + return netdiagKundentext((string) $json); } $parts = array(); foreach ($data as $key => $val) { - 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); - } elseif (is_bool($val)) { - $val = $val ? 'ja' : 'nein'; + $feld = netdiagFeldFuerKunde((string) $key, $val); + if ($feld !== null) { + $parts[] = $feld; } - $parts[] = ucfirst((string) $key).': '.$val; } - return implode(' | ', $parts); + return empty($parts) ? '-' : implode(' | ', $parts); +} + +/** + * Sorgt dafür, dass die nächsten `$hoeheMm` Millimeter noch auf die aktuelle + * Seite passen — sonst wird vorher umgebrochen und optional eine + * Wiederholungszeile (z.B. der Tabellenkopf) ausgegeben. + * + * Der automatische Seitenumbruch von TCPDF reicht dafür nicht: er greift erst, + * wenn die Zelle schon geschrieben wird, und zerreißt dabei zusammengehörige + * Blöcke (Tabellenkopf/Datenzeilen, Messungstitel/Messergebnis). + * + * @param TCPDF $pdf PDF-Objekt + * @param float $hoeheMm benötigte Resthöhe in mm + * @param callable|null $nachUmbruch wird nach einem Seitenwechsel aufgerufen + * @return bool true, wenn umgebrochen wurde + */ +function netdiagPdfEnsureSpace($pdf, $hoeheMm, $nachUmbruch = null) +{ + $rest = ($pdf->getPageHeight() - $pdf->getBreakMargin()) - $pdf->GetY(); + if ($rest >= $hoeheMm) { + return false; + } + $pdf->AddPage(); + if (is_callable($nachUmbruch)) { + $nachUmbruch($pdf); + } + return true; +} + +/** + * Kopfzeile einer Tabelle ausgeben (blau hinterlegt) und danach Schrift/Farbe + * für die Datenzeilen wiederherstellen. Als eigene Funktion, damit dieselbe + * Zeile nach einem Seitenumbruch wiederholt werden kann — vorher standen die + * Spalten ab Seite 2 ohne jede Beschriftung da. + * + * @param TCPDF $pdf PDF-Objekt + * @param array $cols Spaltenbreiten in mm + * @param array $heads Sprachschlüssel der Überschriften + * @param Translate $outputlangs Sprache + * @return void + */ +function netdiagPdfTableHead($pdf, $cols, $heads, $outputlangs) +{ + $pdf->SetFillColor(0, 70, 130); + $pdf->SetTextColor(255, 255, 255); + $pdf->SetFont('', 'B', 8); + foreach ($heads as $idx => $h) { + $pdf->Cell($cols[$idx], 6, $outputlangs->transnoentities($h), 1, 0, 'L', true); + } + $pdf->Ln(); + $pdf->SetTextColor(0, 0, 0); + $pdf->SetFont('', '', 8); } /** diff --git a/netdiagprotocol_card.php b/netdiagprotocol_card.php index 89d4743..3b9c6b7 100644 --- a/netdiagprotocol_card.php +++ b/netdiagprotocol_card.php @@ -182,19 +182,49 @@ print load_fiche_titre($langs->trans("NetDiagDevices").' ('.count($devices).')', print '
'; print ''; print ''; -print ''; +print ''; +print ''; print ''; +// Fundweg als Klartext — "ping"/"port"/"arp"/"mdns" sagt sonst nur dem Entwickler etwas +$foundvialabels = array( + 'ping' => $langs->trans("NetDiagFoundViaPing"), + 'port' => $langs->trans("NetDiagFoundViaPort"), + 'arp' => $langs->trans("NetDiagFoundViaArp"), + 'mdns' => $langs->trans("NetDiagFoundViaMdns"), +); foreach ($devices as $dev) { + // Anzeigename: eigener Name vor mDNS-/Host-/NetBIOS-Name (wie in der App) + $anzeige = ''; + foreach (array($dev->custom_name, $dev->mdns_name, $dev->hostname, $dev->netbios_name) as $kandidat) { + if (!empty($kandidat)) { + $anzeige = $kandidat; + break; + } + } print ''; print ''; print ''; - print ''; + print ''; print ''; print ''; + print ''; + print ''; print ''; } if (empty($devices)) { - print ''; + print ''; } print '
'.$langs->trans("IpAddress").''.$langs->trans("MacAddress").''.$langs->trans("Hostname").''.$langs->trans("Vendor").''.$langs->trans("DeviceType").''.$langs->trans("Hostname").''.$langs->trans("NetDiagVendor").''.$langs->trans("DeviceType").''.$langs->trans("NetDiagOpenPorts").''.$langs->trans("NetDiagFoundVia").'
'.dol_escape_htmltag($dev->ip).''.dol_escape_htmltag($dev->mac).''.dol_escape_htmltag($dev->hostname).''.dol_escape_htmltag($anzeige); + // Weitere Bezeichner nur zeigen, wenn sie vom Anzeigenamen abweichen + $weitere = array(); + if (!empty($dev->netbios_name) && $dev->netbios_name !== $anzeige) { + $weitere[] = 'NB: '.$dev->netbios_name; + } + if (!empty($dev->mdns_name) && $dev->mdns_name !== $anzeige) { + $weitere[] = 'mDNS: '.$dev->mdns_name; + } + if (!empty($weitere)) { + print '
'.dol_escape_htmltag(implode(' · ', $weitere)).''; + } + print '
'.dol_escape_htmltag($dev->vendor).''.dol_escape_htmltag($dev->devicetype).''.dol_escape_htmltag($dev->open_ports).''.dol_escape_htmltag($foundvialabels[$dev->found_via] ?? $dev->found_via).'
-
-
'; diff --git a/sql/llx_netdiag_device.sql b/sql/llx_netdiag_device.sql index b2cfab9..a4ce6ad 100644 --- a/sql/llx_netdiag_device.sql +++ b/sql/llx_netdiag_device.sql @@ -14,8 +14,21 @@ CREATE TABLE llx_netdiag_device( ip varchar(45), mac varchar(17), hostname varchar(255), + -- Weitere Bezeichner, unter denen sich ein Gerät meldet. Ein Drucker ist + -- per DNS oft namenlos, meldet sich aber per mDNS als "Brother HL-L2350DW". + netbios_name varchar(64), + mdns_name varchar(255), + mdns_services varchar(512), + -- vom Techniker vergebener Name; überschreibt in der Anzeige alles andere + custom_name varchar(255), vendor varchar(128), devicetype varchar(64), + -- offene Ports als Komma-Liste (z.B. "80,443,9100") — die Portliste ist die + -- Begründung für die Geräteart und gehört deshalb ins Protokoll + open_ports varchar(255), + -- wie das Gerät gefunden wurde: ping | port | arp | mdns + found_via varchar(16), + last_seen datetime, note text, date_creation datetime NOT NULL, tms timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL diff --git a/sql/llx_netdiag_device_v2.sql b/sql/llx_netdiag_device_v2.sql new file mode 100644 index 0000000..cd4e915 --- /dev/null +++ b/sql/llx_netdiag_device_v2.sql @@ -0,0 +1,26 @@ +-- Copyright (C) 2026 Eduard Wisch +-- +-- 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. +-- +-- Nachtrag zu llx_netdiag_device: Gerätemerkmale, die die App seit dem +-- IP-Scanner-Umbau ermittelt, die aber bisher nirgends ankamen. +-- +-- Der Techniker sah in der App „Drucker HP, Port 9100" — im Kundenprotokoll +-- stand nur die IP. Die Felder fehlten dabei nicht etwa in der Übertragung, +-- sondern durchgängig: in dieser Tabelle, in NetDiagDevice::$fields, im +-- App-Payload und in der Anzeige. Diese Datei ergänzt die Tabelle. +-- +-- Läuft bei jedem Modul-Update mit (DolibarrModules::_load_tables führt alle +-- llx_*.sql aus, alphabetisch — „.sql" vor „_v2.sql"). „IF NOT EXISTS" macht +-- den Lauf wiederholbar, ohne dass ein Update mit Fehler abbricht. + +ALTER TABLE llx_netdiag_device ADD COLUMN IF NOT EXISTS netbios_name varchar(64) AFTER hostname; +ALTER TABLE llx_netdiag_device ADD COLUMN IF NOT EXISTS mdns_name varchar(255) AFTER netbios_name; +ALTER TABLE llx_netdiag_device ADD COLUMN IF NOT EXISTS mdns_services varchar(512) AFTER mdns_name; +ALTER TABLE llx_netdiag_device ADD COLUMN IF NOT EXISTS custom_name varchar(255) AFTER mdns_services; +ALTER TABLE llx_netdiag_device ADD COLUMN IF NOT EXISTS open_ports varchar(255) AFTER devicetype; +ALTER TABLE llx_netdiag_device ADD COLUMN IF NOT EXISTS found_via varchar(16) AFTER open_ports; +ALTER TABLE llx_netdiag_device ADD COLUMN IF NOT EXISTS last_seen datetime AFTER found_via;