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).
275 lines
10 KiB
PHP
275 lines
10 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/netdiagprotocol_card.php
|
|
* \ingroup netdiag
|
|
* \brief Detailansicht eines Diagnose-Protokolls (Backend)
|
|
*/
|
|
|
|
// Dolibarr-Umgebung laden
|
|
$res = 0;
|
|
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
|
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
|
}
|
|
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
|
$tmp2 = realpath(__FILE__);
|
|
$i = strlen($tmp) - 1;
|
|
$j = strlen($tmp2) - 1;
|
|
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
|
$i--;
|
|
$j--;
|
|
}
|
|
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
|
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
|
}
|
|
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
|
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../main.inc.php")) {
|
|
$res = @include "../main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../../main.inc.php")) {
|
|
$res = @include "../../main.inc.php";
|
|
}
|
|
if (!$res) {
|
|
die("Include of main fails");
|
|
}
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
|
|
require_once __DIR__.'/class/netdiagprotocol.class.php';
|
|
require_once __DIR__.'/class/netdiagdevice.class.php';
|
|
require_once __DIR__.'/class/netdiagmeasurement.class.php';
|
|
require_once __DIR__.'/lib/netdiag.lib.php';
|
|
require_once __DIR__.'/lib/netdiag_pdf.lib.php';
|
|
|
|
/**
|
|
* @var Conf $conf
|
|
* @var DoliDB $db
|
|
* @var Translate $langs
|
|
* @var User $user
|
|
*/
|
|
|
|
$langs->loadLangs(array("netdiag@netdiag", "companies", "orders", "other"));
|
|
|
|
if (!$user->hasRight('netdiag', 'protocol', 'read')) {
|
|
accessforbidden();
|
|
}
|
|
|
|
$id = GETPOSTINT('id');
|
|
$action = GETPOST('action', 'aZ09');
|
|
|
|
$object = new NetDiagProtocol($db);
|
|
if ($id <= 0 || $object->fetch($id) <= 0) {
|
|
accessforbidden('Protocol not found');
|
|
}
|
|
|
|
// Aktion: PDF generieren
|
|
if ($action == 'builddoc' && $user->hasRight('netdiag', 'protocol', 'read')) {
|
|
$result = netdiagGeneratePdf($db, $object, $langs);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans("FileGenerated"), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, $object->errors, 'errors');
|
|
}
|
|
header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id);
|
|
exit;
|
|
}
|
|
|
|
// Aktion: löschen
|
|
if ($action == 'confirm_delete' && GETPOST('confirm') == 'yes' && $user->hasRight('netdiag', 'protocol', 'delete')) {
|
|
if ($object->delete($user) > 0) {
|
|
setEventMessages($langs->trans("RecordDeleted"), null, 'mesgs');
|
|
header("Location: ".dol_buildpath('/netdiag/netdiagindex.php', 1));
|
|
exit;
|
|
}
|
|
setEventMessages($object->error, $object->errors, 'errors');
|
|
}
|
|
|
|
/*
|
|
* Ansicht
|
|
*/
|
|
|
|
$form = new Form($db);
|
|
llxHeader('', $object->ref, '', '', 0, 0, '', '', '', 'mod-netdiag page-card');
|
|
|
|
$head = netdiagProtocolPrepareHead($object);
|
|
print dol_get_fiche_head($head, 'card', $langs->trans("NetDiagProtocol"), -1, 'fa-network-wired');
|
|
|
|
// Lösch-Bestätigung
|
|
if ($action == 'delete') {
|
|
print $form->formconfirm($_SERVER["PHP_SELF"]."?id=".$object->id, $langs->trans("Delete"), $langs->trans("ConfirmDeleteObject"), "confirm_delete", '', '', 1);
|
|
}
|
|
|
|
$linkback = '<a href="'.dol_buildpath('/netdiag/netdiagindex.php', 1).'">'.$langs->trans("BackToList").'</a>';
|
|
dol_banner_tab($object, 'id', $linkback, 1, 'rowid', 'ref');
|
|
|
|
print '<div class="fichecenter">';
|
|
print '<div class="underbanner clearboth"></div>';
|
|
print '<table class="border centpercent tableforfield">';
|
|
|
|
print '<tr><td class="titlefield">'.$langs->trans("Label").'</td><td>'.dol_escape_htmltag($object->label).'</td></tr>';
|
|
|
|
$soc = new Societe($db);
|
|
print '<tr><td>'.$langs->trans("ThirdParty").'</td><td>';
|
|
if ($object->fk_soc > 0 && $soc->fetch($object->fk_soc) > 0) {
|
|
print $soc->getNomUrl(1);
|
|
}
|
|
print '</td></tr>';
|
|
|
|
print '<tr><td>'.$langs->trans("Order").'</td><td>';
|
|
if ($object->fk_commande > 0) {
|
|
$cmd = new Commande($db);
|
|
if ($cmd->fetch($object->fk_commande) > 0) {
|
|
print $cmd->getNomUrl(1);
|
|
}
|
|
}
|
|
print '</td></tr>';
|
|
|
|
print '<tr><td>'.$langs->trans("DateDiag").'</td><td>'.dol_print_date($object->date_diag, 'dayhour').'</td></tr>';
|
|
print '<tr><td>'.$langs->trans("Location").'</td><td>'.dol_escape_htmltag($object->standort).'</td></tr>';
|
|
print '<tr><td>'.$langs->trans("Subnet").'</td><td>'.dol_escape_htmltag($object->subnet).'</td></tr>';
|
|
print '<tr><td>'.$langs->trans("Status").'</td><td>'.$object->getLibStatut(4).'</td></tr>';
|
|
print '<tr><td>'.$langs->trans("Note").'</td><td>'.dol_nl2br(dol_escape_htmltag($object->note)).'</td></tr>';
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
|
|
print dol_get_fiche_end();
|
|
|
|
// Aktionsknöpfe
|
|
print '<div class="tabsAction">';
|
|
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=builddoc&token='.newToken().'">'.$langs->trans("BuildDoc").'</a>';
|
|
if ($user->hasRight('netdiag', 'protocol', 'delete')) {
|
|
print '<a class="butActionDelete" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=delete&token='.newToken().'">'.$langs->trans("Delete").'</a>';
|
|
}
|
|
print '</div>';
|
|
|
|
// Vorhandene PDF-Dokumente
|
|
$upload_dir = netdiagGetOutputDir().'/'.dol_sanitizeFileName($object->ref);
|
|
if (is_dir($upload_dir)) {
|
|
$files = dol_dir_list($upload_dir, 'files', 0, '\.pdf$');
|
|
if (!empty($files)) {
|
|
print '<br><table class="noborder centpercent"><tr class="liste_titre"><th>'.$langs->trans("Documents").'</th></tr>';
|
|
foreach ($files as $f) {
|
|
$durl = DOL_URL_ROOT.'/document.php?modulepart=netdiag&file='.urlencode(dol_sanitizeFileName($object->ref).'/'.$f['name']);
|
|
print '<tr class="oddeven"><td>'.img_mime($f['name']).' <a href="'.$durl.'">'.dol_escape_htmltag($f['name']).'</a></td></tr>';
|
|
}
|
|
print '</table>';
|
|
}
|
|
}
|
|
|
|
// Geräteliste
|
|
$devObj = new NetDiagDevice($db);
|
|
$devices = $devObj->fetchAllByProtocol($object->id);
|
|
print '<br>';
|
|
print load_fiche_titre($langs->trans("NetDiagDevices").' ('.count($devices).')', '', 'fa-desktop');
|
|
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("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($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="7" class="opacitymedium center">-</td></tr>';
|
|
}
|
|
print '</table></div>';
|
|
|
|
// Messungen
|
|
$measObj = new NetDiagMeasurement($db);
|
|
$measurements = $measObj->fetchAllByProtocol($object->id);
|
|
$statuslabels = array(
|
|
0 => 'NetDiagMeasureOk',
|
|
1 => 'NetDiagMeasureWarn',
|
|
2 => 'NetDiagMeasureFail',
|
|
3 => 'NetDiagMeasureUnmeasurable',
|
|
);
|
|
$statuscss = array(
|
|
0 => 'badge-status4',
|
|
1 => 'badge-status1',
|
|
2 => 'badge-status8',
|
|
3 => 'badge-status0', // grau: Test war nicht durchfuehrbar
|
|
);
|
|
print '<br>';
|
|
print load_fiche_titre($langs->trans("NetDiagMeasurements").' ('.count($measurements).')', '', 'fa-wave-square');
|
|
print '<div class="div-table-responsive-no-min"><table class="noborder centpercent">';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("DateMeasure").'</th><th>'.$langs->trans("ToolCategory").'</th>';
|
|
print '<th>'.$langs->trans("Tool").'</th><th>'.$langs->trans("Label").'</th>';
|
|
print '<th>'.$langs->trans("Result").'</th><th class="center">'.$langs->trans("MeasureStatus").'</th>';
|
|
print '</tr>';
|
|
foreach ($measurements as $m) {
|
|
$st = (int) $m->measure_status;
|
|
// Unbekannte Werte nie als OK durchgehen lassen (Array-Zugriff waere sonst leer)
|
|
if ($st < 0 || $st > 3) {
|
|
$st = 1;
|
|
}
|
|
print '<tr class="oddeven">';
|
|
print '<td class="nowraponall">'.dol_print_date($m->date_measure, 'dayhour').'</td>';
|
|
print '<td>'.dol_escape_htmltag($m->category).'</td>';
|
|
print '<td>'.dol_escape_htmltag($m->tool).'</td>';
|
|
print '<td>'.dol_escape_htmltag($m->label).'</td>';
|
|
print '<td>'.netdiagFormatResult($m->result, $m->tool).'</td>';
|
|
print '<td class="center"><span class="badge '.$statuscss[$st].'">'.$langs->trans($statuslabels[$st]).'</span></td>';
|
|
print '</tr>';
|
|
}
|
|
if (empty($measurements)) {
|
|
print '<tr><td colspan="6" class="opacitymedium center">-</td></tr>';
|
|
}
|
|
print '</table></div>';
|
|
|
|
llxFooter();
|
|
$db->close();
|