Phase 5: Kundendokument lesbar, Gerätemerkmale kommen endlich an
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).
This commit is contained in:
parent
8b433726f3
commit
0b3db47ec1
10 changed files with 432 additions and 37 deletions
|
|
@ -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<int,string>
|
||||
*/
|
||||
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 !== '';
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
|||
'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,
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -289,3 +289,144 @@ function netdiagFormatWifiKanal($data)
|
|||
$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', ''),
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
$feld = netdiagFeldFuerKunde((string) $key, $val);
|
||||
if ($feld !== null) {
|
||||
$parts[] = $feld;
|
||||
}
|
||||
$val = implode(', ', $flat);
|
||||
} elseif (is_bool($val)) {
|
||||
$val = $val ? 'ja' : 'nein';
|
||||
}
|
||||
$parts[] = ucfirst((string) $key).': '.$val;
|
||||
return empty($parts) ? '-' : implode(' | ', $parts);
|
||||
}
|
||||
return 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<int,int> $cols Spaltenbreiten in mm
|
||||
* @param array<int,string> $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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -182,19 +182,49 @@ print load_fiche_titre($langs->trans("NetDiagDevices").' ('.count($devices).')',
|
|||
print '<div class="div-table-responsive-no-min"><table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("IpAddress").'</th><th>'.$langs->trans("MacAddress").'</th>';
|
||||
print '<th>'.$langs->trans("Hostname").'</th><th>'.$langs->trans("Vendor").'</th><th>'.$langs->trans("DeviceType").'</th>';
|
||||
print '<th>'.$langs->trans("Hostname").'</th><th>'.$langs->trans("NetDiagVendor").'</th><th>'.$langs->trans("DeviceType").'</th>';
|
||||
print '<th>'.$langs->trans("NetDiagOpenPorts").'</th><th>'.$langs->trans("NetDiagFoundVia").'</th>';
|
||||
print '</tr>';
|
||||
// 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 '<tr class="oddeven">';
|
||||
print '<td>'.dol_escape_htmltag($dev->ip).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($dev->mac).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($dev->hostname).'</td>';
|
||||
print '<td>'.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 '<br><span class="opacitymedium small">'.dol_escape_htmltag(implode(' · ', $weitere)).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '<td>'.dol_escape_htmltag($dev->vendor).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($dev->devicetype).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($dev->open_ports).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($foundvialabels[$dev->found_via] ?? $dev->found_via).'</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
if (empty($devices)) {
|
||||
print '<tr><td colspan="5" class="opacitymedium center">-</td></tr>';
|
||||
print '<tr><td colspan="7" class="opacitymedium center">-</td></tr>';
|
||||
}
|
||||
print '</table></div>';
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
26
sql/llx_netdiag_device_v2.sql
Normal file
26
sql/llx_netdiag_device_v2.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- 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.
|
||||
--
|
||||
-- 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;
|
||||
Loading…
Reference in a new issue