"0 DHCP-Server (!)" in Rot: In der Produktionsdatenbank stehen drei
ausgelieferte Messungen des entfernten Werkzeugs dhcpcheck mit Ergebnis
{"count":0,"server":[],"hinweis":""} und Status 2. Bei Kabelverbindung gibt
Android die DHCP-Angaben aber grundsaetzlich nicht heraus (nur DhcpInfo, nur
WLAN, deprecated) - gemessen wurde also nichts, "0 gefunden" war der
Rueckgabewert fuer "nicht ermittelbar". netdiagAltlastKorrektur() zeigt diese
Messungen jetzt als "nicht messbar" mit Erklaerung, in PDF und
Technikeransicht. Rohdaten bleiben unangetastet, korrigiert wird nur die
Darstellung - und die wird bei jedem Abruf neu erzeugt. Die Ergebniszahlen
werden dabei unterdrueckt: "Gefundene Geraete: 0" widerspraeche der Aussage
direkt darueber.
"Lease-Dauer: 864000 s s": aeltere App-Fassungen haben die Einheit in den Wert
geschrieben, die Feldtabelle haengt sie erneut an (Prod-Messung #42).
Adress- und Lease-Felder mit dem Wert 0 erscheinen jetzt als "nicht
ermittelbar" statt als "0" - das war nie eine Messung, sondern das
"nichts ermittelt" der alten Android-API.
Neu in der Whitelist: dhcpQuelle (system = LinkProperties ab Android 11 fuer
jeden Anschlusstyp, wlan = alter Weg nur fuer WLAN). Damit ist klaerbar, ob
eine fehlende DHCP-Angabe an der Leitung lag oder an der Android-Version.
Geprueft gegen die lokale Testinstanz mit einer Kopie der echten
Prod-Messungen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
714 lines
26 KiB
PHP
714 lines
26 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 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);
|
||
} 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<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);
|
||
}
|
||
|
||
/**
|
||
* 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');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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');
|
||
}
|
||
}
|