* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file netdiag/lib/netdiag_pdf.lib.php * \ingroup netdiag * \brief PDF-Generator für Diagnose-Protokolle */ require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php'; require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php'; require_once __DIR__.'/netdiag.lib.php'; require_once __DIR__.'/../class/netdiagdevice.class.php'; require_once __DIR__.'/../class/netdiagmeasurement.class.php'; /** * Ein Diagnose-Protokoll als PDF erzeugen und im Dokumentenverzeichnis ablegen. * * @param DoliDB $db Datenbank-Handler * @param NetDiagProtocol $protocol Protokoll-Objekt (geladen) * @param Translate $outputlangs Sprache der Ausgabe * @return int >0 wenn OK, <=0 bei Fehler */ function netdiagGeneratePdf($db, $protocol, $outputlangs) { global $conf, $mysoc, $langs; $outputlangs->loadLangs(array("netdiag@netdiag", "main", "companies", "orders")); $dir = netdiagGetOutputDir().'/'.dol_sanitizeFileName($protocol->ref); if (!dol_is_dir($dir)) { if (dol_mkdir($dir) < 0) { $protocol->error = 'Konnte Verzeichnis nicht anlegen: '.$dir; return -1; } } $file = $dir.'/'.dol_sanitizeFileName($protocol->ref).'.pdf'; // Daten laden $soc = new Societe($db); if ($protocol->fk_soc > 0) { $soc->fetch($protocol->fk_soc); } $devObj = new NetDiagDevice($db); $devices = $devObj->fetchAllByProtocol($protocol->id); $measObj = new NetDiagMeasurement($db); $measurements = $measObj->fetchAllByProtocol($protocol->id); // PDF aufbauen $pdf = pdf_getInstance('A4', 'mm', 'P'); $default_font_size = pdf_getPDFFontSize($outputlangs); $pdf->SetAutoPageBreak(true, 20); $pdf->SetMargins(15, 15, 15); $pdf->SetTitle($protocol->ref); $pdf->SetSubject($outputlangs->transnoentities("NetDiagProtocol")); $pdf->SetCreator("Dolibarr ".DOL_VERSION." - NetDiag"); $pdf->SetAuthor($mysoc->name); $pdf->AddPage(); // Kopf $pdf->SetFont('', 'B', 16); $pdf->SetTextColor(0, 70, 130); $pdf->Cell(0, 8, $outputlangs->transnoentities("NetDiagProtocol").' '.$protocol->ref, 0, 1, 'L'); $pdf->SetTextColor(0, 0, 0); $pdf->SetFont('', '', 9); $pdf->Cell(0, 5, $mysoc->name, 0, 1, 'L'); $pdf->Ln(3); // Stammdaten $pdf->SetFont('', '', 10); $infos = array( $outputlangs->transnoentities("ThirdParty") => ($soc->id > 0 ? $soc->name : '-'), $outputlangs->transnoentities("DateDiag") => dol_print_date($protocol->date_diag, 'dayhour', false, $outputlangs), $outputlangs->transnoentities("Location") => ($protocol->standort ? $protocol->standort : '-'), $outputlangs->transnoentities("Subnet") => ($protocol->subnet ? $protocol->subnet : '-'), ); foreach ($infos as $k => $v) { $pdf->SetFont('', 'B', 10); $pdf->Cell(40, 6, $k.':', 0, 0, 'L'); $pdf->SetFont('', '', 10); $pdf->Cell(0, 6, $v, 0, 1, 'L'); } if (!empty($protocol->note)) { $pdf->Ln(1); $pdf->SetFont('', 'I', 9); $pdf->MultiCell(0, 5, $outputlangs->transnoentities("Note").': '.$protocol->note, 0, 'L'); } $pdf->Ln(4); // Geräteliste $pdf->SetFont('', 'B', 12); $pdf->Cell(0, 7, $outputlangs->transnoentities("NetDiagDevices").' ('.count($devices).')', 0, 1, 'L'); /* * 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) { 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'); } $pdf->Ln(4); // Messungen $pdf->SetFont('', 'B', 12); $pdf->Cell(0, 7, $outputlangs->transnoentities("NetDiagMeasurements").' ('.count($measurements).')', 0, 1, 'L'); // Status 3 = "nicht messbar": der Test konnte nicht durchgeführt werden. // Bewusst grau und mit eigenem Zeichen — weder grün (wäre gelogen) noch rot // (wäre eine Aussage über das Kundennetz, die die Messung nicht hergibt). $statuslabels = array( 0 => $outputlangs->transnoentities("NetDiagMeasureOk"), 1 => $outputlangs->transnoentities("NetDiagMeasureWarn"), 2 => $outputlangs->transnoentities("NetDiagMeasureFail"), 3 => $outputlangs->transnoentities("NetDiagMeasureUnmeasurable"), ); // Zusätzlich ein Zeichen, damit die Ampel auch im Schwarz-Weiß-Ausdruck lesbar // bleibt. Bei Status 0 bewusst leer: das Label heißt dort bereits "OK", sonst // stünde im PDF doppelt "OK OK". $statusmarks = array(0 => '', 1 => '! ', 2 => 'X ', 3 => '? '); foreach ($measurements as $m) { // Bekannte Falschaussagen aus Altmessungen geraderücken (siehe // netdiagAltlastKorrektur() — betrifft das entfernte Werkzeug dhcpcheck, // dessen „0 DHCP-Server (!)" in Rot in ausgelieferten Protokollen steht). $korr = netdiagAltlastKorrektur($m->tool, $m->measure_status, $m->label, $m->result); $st = (int) $korr['status']; if ($st < 0 || $st > 3) { $st = 1; // unbekannter Wert -> als Warnung behandeln, nie als OK } if ($st == 2) { $pdf->SetFillColor(230, 130, 130); } elseif ($st == 1) { $pdf->SetFillColor(245, 215, 130); } elseif ($st == 3) { $pdf->SetFillColor(210, 210, 210); } 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); // Klarname statt interner ID — „ipscan" sagt dem Kunden nichts $title = netdiagToolName($m->tool).($korr['label'] ? ' — '.$korr['label'] : ''); $pdf->Cell(150, 6, dol_trunc($title, 80), 1, 0, 'L'); $pdf->Cell(30, 6, $statusmarks[$st].$statuslabels[$st], 1, 1, 'C', true); $pdf->SetFont('', '', 8); // Messparameter: ohne sie steht das Ergebnis ohne Bezug da — man sieht // nicht, gegen welches Ziel gemessen wurde (Prod-Messung #126). if ($korr['hinweis'] !== '') { $pdf->SetFont('', 'I', 7); $pdf->MultiCell(180, 4, netdiagPdfText($korr['hinweis']), 1, 'L'); $pdf->SetFont('', '', 8); } $params = netdiagFormatParams($m->params); if ($params !== '') { $pdf->SetFont('', 'I', 7); $pdf->MultiCell(180, 4, netdiagPdfText($params), 1, 'L'); $pdf->SetFont('', '', 8); } /* * Bei einer als Altlast korrigierten Messung KEINE Zahlen mehr drucken: * „Gefundene Geräte: 0 | DHCP-Server: keine" widerspricht der Aussage * „nicht messbar" direkt darüber und liest sich wieder wie ein Befund. * Der Erklärtext steht bereits hinter der Titelzeile. */ if ($korr['hinweis'] !== '') { continue; } if ($m->tool === 'stresstest') { netdiagPdfStressTest($pdf, $m->result); } elseif ($m->tool === 'wifikanal') { netdiagPdfWifiKanal($pdf, $m->result); } elseif ($m->tool === 'monitor') { netdiagPdfMonitor($pdf, $m->result); } elseif ($m->tool === 'internetkette') { netdiagPdfKette($pdf, $m->result); } elseif ($m->tool === 'dnscheck') { netdiagPdfDns($pdf, $m->result); } else { $pdf->MultiCell(180, 5, netdiagPdfFlattenResult($m->result), 1, 'L'); } } if (empty($measurements)) { $pdf->SetFont('', '', 9); $pdf->Cell(180, 5, '-', 1, 1, 'C'); } // Fuß $pdf->Ln(6); $pdf->SetFont('', 'I', 8); $pdf->SetTextColor(120, 120, 120); $pdf->Cell(0, 5, $outputlangs->transnoentities("NetDiagProtocol").' '.$protocol->ref.' — '.dol_print_date(dol_now(), 'dayhour', false, $outputlangs), 0, 1, 'L'); // Speichern $pdf->Output($file, 'F'); dolChmod($file); return 1; } /** * Zeichen ersetzen, die der PDF-Standardzeichensatz nicht darstellen kann. * * `pdf_getPDFFont()` liefert in dieser Dolibarr-Installation Helvetica — einen * Core-Font mit WinAnsi-Kodierung, kein Unicode. Alles außerhalb dieses Vorrats * druckt TCPDF als „?", ohne zu warnen (bekannt aus KB #1025, dort mit dem * Ankreuzkästchen ☐). * * Aufgefallen am Pfeil in der Internet-Kette: aus * „Namensauflösung: www.google.de → 142.250.185.67" wurde im Kundendokument * „www.google.de ? 142.250.185.67" — der Pfeil ist dort kein Schmuck, er * trennt Frage und Antwort. * * Bewusst eine Ersetzung statt einer Unicode-Schrift: `dejavusans` würde jedes * Zeichen können, aber Schriftbild und Dateigröße aller Protokolle ändern. * Gedankenstrich, Mittelpunkt, Auslassungspunkte und Anführungszeichen sind in * WinAnsi enthalten und bleiben unangetastet (im Dokument nachgeprüft). * * @param string $s Text aus der App oder aus Altdaten * @return string druckbarer Text */ function netdiagPdfText($s) { return strtr((string) $s, array( '→' => '->', '←' => '<-', '↔' => '<->', '⇒' => '=>', '✓' => 'ja', '✔' => 'ja', '✗' => 'nein', '✘' => 'nein', '≤' => '<=', '≥' => '>=', '≠' => '!=', '☐' => '[ ]', '☑' => '[x]', '⚠' => '!', '≈' => 'ca. ', )); } /** * Mess-Ergebnis (JSON) als einzeiligen Text für PDF-Ausgabe. * * @param string $json JSON-String * @return string Lesbarer Text */ function netdiagPdfFlattenResult($json) { if (empty($json)) { return '-'; } $data = json_decode($json, true); if (!is_array($data)) { return netdiagPdfText(netdiagKundentext((string) $json)); } $parts = array(); foreach ($data as $key => $val) { $feld = netdiagFeldFuerKunde((string) $key, $val); if ($feld !== null) { $parts[] = $feld; } } return empty($parts) ? '-' : netdiagPdfText(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); } /** * Dauer-/Stresstest strukturiert ins PDF schreiben statt als Fließtext. * * netdiagPdfFlattenResult() würde Kennzahlen, Ausfallliste und Zeitreihe zu * einem einzigen langen Absatz zusammenkleben — bei einer Stunde Laufzeit mit * 60 Minuten-Buckets kaum lesbar. Hier stattdessen eine kompakte * Kennzahlenzeile plus eine echte Ausfalltabelle. Die Zeitreihe (`verlauf`) * wird bewusst NICHT gedruckt — sie ist als Diagramm in der App nützlich, * als reiner Text auf Papier nicht. * * @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile) * @param string $json result-JSON der Messung * @return void */ function netdiagPdfStressTest($pdf, $json) { $data = json_decode((string) $json, true); if (!is_array($data)) { $pdf->MultiCell(180, 5, (string) $json, 1, 'L'); return; } if (!empty($data['hinweis']) || !empty($data['fehler'])) { $pdf->MultiCell(180, 5, (string) ($data['hinweis'] ?? $data['fehler']), 1, 'L'); return; } $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'; } $pdf->MultiCell(180, 5, implode(' | ', $sum), 1, 'L'); // Ausfalltabelle — je Zeile "HH:MM:SS – HH:MM:SS (Xs)", vorformatiert von // der App (dieselben Zeilen, die auch in der Messungen-Liste in der App // stehen — eine Quelle der Wahrheit statt zwei Formatierungen). $ausfaelle = (isset($data['ausfaelle']) && is_array($data['ausfaelle'])) ? $data['ausfaelle'] : array(); if (empty($ausfaelle)) { $pdf->SetFont('', 'I', 8); $pdf->Cell(180, 5, 'Kein Ausfall während des Laufs.', 1, 1, 'L'); $pdf->SetFont('', '', 8); return; } $pdf->SetFont('', 'B', 8); $pdf->Cell(180, 5, 'Ausfälle ('.count($ausfaelle).')', 1, 1, 'L'); $pdf->SetFont('', '', 8); foreach ($ausfaelle as $line) { $pdf->Cell(180, 5, dol_trunc(netdiagPdfText($line), 120), 1, 1, 'L'); } } /** * DNS-Prüfung ins PDF schreiben: je Server eine eigene Zeile. * * Dieselbe Begründung wie beim Monitor und bei der Internet-Kette: die * generische Darstellung schiebt alle Serverantworten zu einer `|`-Kette * zusammen. Bei zwei Servern geht das noch, bei den vier bis fünf Einträgen * eines Firmennetzes ist die eigentliche Aussage — welcher Server antwortet * und welcher nicht — nicht mehr auffindbar. * * @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile) * @param string $json result-JSON der Messung * @return void */ function netdiagPdfDns($pdf, $json) { $data = json_decode((string) $json, true); if (!is_array($data)) { $pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L'); return; } $sum = array(); if (!empty($data['ziel'])) { $sum[] = 'Abgefragter Name: '.$data['ziel']; } if (!empty($data['gefragteServer']) && is_array($data['gefragteServer'])) { $sum[] = 'Befragte Server: '.count($data['gefragteServer']); } if (!empty($sum)) { $pdf->MultiCell(180, 5, netdiagPdfText(implode(' | ', $sum)), 1, 'L'); } $antworten = (isset($data['antworten']) && is_array($data['antworten'])) ? $data['antworten'] : array(); if (!empty($antworten)) { netdiagPdfEnsureSpace($pdf, 10); $pdf->SetFont('', 'B', 8); $pdf->Cell(180, 5, 'Antworten der Server ('.count($antworten).')', 1, 1, 'L'); $pdf->SetFont('', '', 8); foreach ($antworten as $line) { netdiagPdfEnsureSpace($pdf, 5, function ($p) { $p->SetFont('', 'B', 8); $p->Cell(180, 5, 'Antworten der Server — Fortsetzung', 1, 1, 'L'); $p->SetFont('', '', 8); }); // MultiCell, weil eine Fehlermeldung des Servers lang werden kann $pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext((string) $line)), 1, 'L'); } } // Messverfahren und Hinweis zum Schluss — die Grenze der Aussage gehört // ins Kundendokument (gemessen wurde je ein Paket an je einen Server). foreach (array('hinweis', 'verfahren') as $feld) { if (!empty($data[$feld])) { netdiagPdfEnsureSpace($pdf, 8); $pdf->SetFont('', 'I', 7); $pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data[$feld])), 1, 'L'); $pdf->SetFont('', '', 8); } } } /** * Internet-Kette ins PDF schreiben: Fehlerglied zuerst, dann die Stufen. * * Ohne eigenen Zweig stünden die vier Stufen als `|`-Kette in einer Zelle und * das Fehlerglied — die eigentliche Aussage — mittendrin. Beim Kunden wird * genau diese eine Zeile gelesen: „woran liegt es". * * @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile) * @param string $json result-JSON der Messung * @return void */ function netdiagPdfKette($pdf, $json) { $data = json_decode((string) $json, true); if (!is_array($data)) { $pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L'); return; } // Fehlerglied als eigene, hervorgehobene Zeile $glied = isset($data['fehlerglied']) ? trim((string) $data['fehlerglied']) : ''; if ($glied !== '') { $pdf->SetFont('', 'B', 8); $pdf->Cell(180, 5, $glied === 'keines' ? 'Kein Glied der Kette hat versagt.' : 'Fehlerglied: '.$glied, 1, 1, 'L'); $pdf->SetFont('', '', 8); } $stufen = (isset($data['stufen']) && is_array($data['stufen'])) ? $data['stufen'] : array(); if (!empty($stufen)) { netdiagPdfEnsureSpace($pdf, 10); $pdf->SetFont('', 'B', 8); $pdf->Cell(180, 5, 'Geprüfte Stufen', 1, 1, 'L'); $pdf->SetFont('', '', 8); foreach ($stufen as $line) { netdiagPdfEnsureSpace($pdf, 5, function ($p) { $p->SetFont('', 'B', 8); $p->Cell(180, 5, 'Geprüfte Stufen — Fortsetzung', 1, 1, 'L'); $p->SetFont('', '', 8); }); // MultiCell statt Cell: die Stufenzeilen enthalten Fehlertexte des // Systems („Unable to resolve host …"), die auch mal lang werden. // Abschneiden würde ausgerechnet die Begründung kappen. $pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext((string) $line)), 1, 'L'); } } if (!empty($data['systemUrteil'])) { netdiagPdfEnsureSpace($pdf, 8); $pdf->SetFont('', 'I', 7); $pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data['systemUrteil'])), 1, 'L'); $pdf->SetFont('', '', 8); } if (!empty($data['hinweis'])) { netdiagPdfEnsureSpace($pdf, 8); $pdf->SetFont('', 'I', 7); $pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data['hinweis'])), 1, 'L'); $pdf->SetFont('', '', 8); } } /** * Geräte-Dauerüberwachung strukturiert ins PDF schreiben. * * Dritter eigener Zweig neben Dauertest und WLAN-Kanal, aus demselben Grund: * `netdiagPdfFlattenResult()` schiebt alles zu EINER `|`-getrennten Zeile * zusammen. Bei zehn überwachten Geräten standen damit die zehn Zeilen aus * `geraete` UND sämtliche Einzelausfälle in einer einzigen Tabellenzelle — * vollständig, aber beim Kunden nicht lesbar. * * ACHTUNG, Unterschied zu netdiagPdfStressTest(): dort führt ein gesetztes * `hinweis` zum sofortigen Abbruch. Hier darf es das NICHT — der Monitor legt * IMMER einen Hinweis an (das Messverfahren gehört zur Aussage dazu, siehe * `buildMonitorMeasurement()` in app/src/lib/monitor.ts). Ein Frühabbruch * würde also bei jedem Monitor-Lauf sämtliche Zahlen verschlucken. Der Hinweis * steht deshalb am Ende, nur `fehler` bricht ab. * * @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile) * @param string $json result-JSON der Messung * @return void */ function netdiagPdfMonitor($pdf, $json) { $data = json_decode((string) $json, true); if (!is_array($data)) { $pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L'); return; } if (!empty($data['fehler']) || !empty($data['error'])) { $pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext((string) ($data['fehler'] ?? $data['error']))), 1, 'L'); return; } // Kennzahlenzeile. Bezeichnungen bewusst wie in netdiagKundenfelder(), // damit dasselbe Feld im PDF und in der Dolibarr-Ansicht gleich heißt. $sum = array(); if (isset($data['dauerSekunden'])) { $sum[] = 'Messdauer: '.netdiagDauerLesbar($data['dauerSekunden']); } if (isset($data['intervallSek'])) { $sum[] = 'Messabstand: '.$data['intervallSek'].' s'; } if (isset($data['geraeteAnzahl'])) { $sum[] = 'Überwachte Geräte: '.$data['geraeteAnzahl']; } if (isset($data['verfuegbarkeitProzent']) && $data['verfuegbarkeitProzent'] !== null) { $sum[] = 'Verfügbarkeit (schlechtestes Gerät): '.number_format((float) $data['verfuegbarkeitProzent'], 1, ',', '').' %'; } if (isset($data['aussetzer'])) { $sum[] = 'Aussetzer: '.$data['aussetzer']; } if (!empty($data['ausfallzeitSek'])) { $sum[] = 'Ausfallzeit (schlechtestes Gerät): '.netdiagDauerLesbar($data['ausfallzeitSek']); } if (!empty($data['laengsterAusfallSek'])) { $sum[] = 'Längster Ausfall: '.netdiagDauerLesbar($data['laengsterAusfallSek']); } if (!empty($sum)) { $pdf->MultiCell(180, 5, implode(' | ', $sum), 1, 'L'); } // Je Gerät eine eigene Zeile — das ist der eigentliche Beleg. Die Zeilen // kommen vorformatiert aus der App (dieselben, die dort in der Messungs- // liste stehen), wie beim Dauertest: eine Quelle der Wahrheit. $geraete = (isset($data['geraete']) && is_array($data['geraete'])) ? $data['geraete'] : array(); if (!empty($geraete)) { netdiagPdfEnsureSpace($pdf, 10); $pdf->SetFont('', 'B', 8); $pdf->Cell(180, 5, 'Überwachte Geräte ('.count($geraete).')', 1, 1, 'L'); $pdf->SetFont('', '', 8); foreach ($geraete as $line) { netdiagPdfEnsureSpace($pdf, 5, function ($p) use ($geraete) { $p->SetFont('', 'B', 8); $p->Cell(180, 5, 'Überwachte Geräte ('.count($geraete).') — Fortsetzung', 1, 1, 'L'); $p->SetFont('', '', 8); }); $pdf->Cell(180, 5, dol_trunc(netdiagPdfText($line), 120), 1, 1, 'L'); } } // Einzelne Ausfälle mit Uhrzeit. Genau das ist der Nachweis bei der // häufigsten Reklamation („die Kamera fällt ständig aus"). $ausfaelle = (isset($data['ausfaelle']) && is_array($data['ausfaelle'])) ? $data['ausfaelle'] : array(); netdiagPdfEnsureSpace($pdf, 10); if (empty($ausfaelle)) { $pdf->SetFont('', 'I', 8); $pdf->Cell(180, 5, 'Kein Ausfall während der Überwachung.', 1, 1, 'L'); $pdf->SetFont('', '', 8); } else { $pdf->SetFont('', 'B', 8); $pdf->Cell(180, 5, 'Einzelne Ausfälle ('.count($ausfaelle).')', 1, 1, 'L'); $pdf->SetFont('', '', 8); /* * Obergrenze gegen den pathologischen Fall: ein im Sekundentakt * flappendes Gerät erzeugt über einen Arbeitstag vierstellig viele * Segmente — das wären zweistellig viele Seiten reine Ausfallliste. * 200 Zeilen sind rund vier Seiten und belegen jede reale Störung * ausreichend. Gekürzt wird NICHT stillschweigend: die Restzahl steht * sichtbar im Dokument, sonst läse sich das Protokoll als vollständig. */ $max = 200; $i = 0; foreach ($ausfaelle as $line) { if ($i >= $max) { break; } netdiagPdfEnsureSpace($pdf, 5, function ($p) use ($ausfaelle) { $p->SetFont('', 'B', 8); $p->Cell(180, 5, 'Einzelne Ausfälle ('.count($ausfaelle).') — Fortsetzung', 1, 1, 'L'); $p->SetFont('', '', 8); }); $pdf->Cell(180, 5, dol_trunc(netdiagPdfText($line), 120), 1, 1, 'L'); $i++; } $rest = count($ausfaelle) - $max; if ($rest > 0) { netdiagPdfEnsureSpace($pdf, 5); $pdf->SetFont('', 'I', 8); $pdf->Cell(180, 5, '… und '.$rest.' weitere Ausfälle, hier aus Platzgründen nicht einzeln aufgeführt.', 1, 1, 'L'); $pdf->SetFont('', '', 8); } } // Hinweis zum Schluss: Messverfahren und ggf. abgerissener Lauf. if (!empty($data['hinweis'])) { netdiagPdfEnsureSpace($pdf, 10); $pdf->SetFont('', 'I', 7); $pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data['hinweis'])), 1, 'L'); $pdf->SetFont('', '', 8); } } /** * WLAN-Kanal-Momentaufnahme im PDF als Tabelle ausgeben. * * Wie netdiagPdfStressTest ein eigener Zweig, weil die generische * Flach-Darstellung die Netzliste (Array von Objekten) unlesbar macht. * * @param TCPDF $pdf PDF-Objekt * @param string $json JSON-String des Ergebnisses * @return void */ function netdiagPdfWifiKanal($pdf, $json) { $data = json_decode($json, true); if (!is_array($data)) { $pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L'); return; } // Aus dem Demomodus der App — im PDF besonders wichtig zu kennzeichnen, // weil das Dokument beim Kunden landet. if (!empty($data['demodaten'])) { $pdf->SetFont('', 'B', 8); $pdf->MultiCell(180, 5, 'DEMODATEN — keine echte Messung', 1, 'L'); $pdf->SetFont('', '', 8); } // Kopfzeile: Anzahl, eigenes Netz, Empfehlung $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 (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)) { $pdf->MultiCell(180, 5, implode(' | ', $sum), 1, 'L'); } if (!empty($data['warnungen']) && is_array($data['warnungen'])) { foreach ($data['warnungen'] as $w) { $pdf->MultiCell(180, 5, '! '.dol_trunc(netdiagPdfText($w), 160), 1, 'L'); } } $netze = (isset($data['netze']) && is_array($data['netze'])) ? $data['netze'] : array(); if (empty($netze)) { return; } // Tabellenkopf $pdf->SetFont('', 'B', 7); $pdf->Cell(60, 5, 'SSID', 1, 0, 'L'); $pdf->Cell(18, 5, 'Kanal', 1, 0, 'C'); $pdf->Cell(22, 5, 'Band', 1, 0, 'L'); $pdf->Cell(20, 5, 'Breite', 1, 0, 'C'); $pdf->Cell(35, 5, 'Standard', 1, 0, 'L'); $pdf->Cell(25, 5, 'Pegel', 1, 1, 'R'); $pdf->SetFont('', '', 7); foreach ($netze as $n) { if (!is_array($n)) { continue; } $pdf->Cell(60, 5, dol_trunc((string) ($n['ssid'] ?? ''), 34), 1, 0, 'L'); $pdf->Cell(18, 5, (string) ($n['kanal'] ?? ''), 1, 0, 'C'); $pdf->Cell(22, 5, (string) ($n['band'] ?? ''), 1, 0, 'L'); $pdf->Cell(20, 5, (isset($n['breiteMhz']) && $n['breiteMhz'] !== null) ? $n['breiteMhz'].' MHz' : '-', 1, 0, 'C'); $pdf->Cell(35, 5, dol_trunc((string) ($n['standard'] ?? '-'), 20), 1, 0, 'L'); $pdf->Cell(25, 5, (string) ($n['rssi'] ?? '').' dBm', 1, 1, 'R'); } }