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

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

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

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

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

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

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

590 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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) {
$st = (int) $m->measure_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).($m->label ? ' — '.$m->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).
$params = netdiagFormatParams($m->params);
if ($params !== '') {
$pdf->SetFont('', 'I', 7);
$pdf->MultiCell(180, 4, $params, 1, 'L');
$pdf->SetFont('', '', 8);
}
if ($m->tool === 'stresstest') {
netdiagPdfStressTest($pdf, $m->result);
} elseif ($m->tool === 'wifikanal') {
netdiagPdfWifiKanal($pdf, $m->result);
} elseif ($m->tool === 'monitor') {
netdiagPdfMonitor($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;
}
/**
* 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 netdiagKundentext((string) $json);
}
$parts = array();
foreach ($data as $key => $val) {
$feld = netdiagFeldFuerKunde((string) $key, $val);
if ($feld !== null) {
$parts[] = $feld;
}
}
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<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((string) $line, 120), 1, 1, 'L');
}
}
/**
* 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, 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((string) $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((string) $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, 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((string) $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');
}
}