dolibarr.netdiag/api/protocols.php
Eduard Wisch 0b3db47ec1 Phase 5: Kundendokument lesbar, Gerätemerkmale kommen endlich an
Vorab: drei Punkte der Roadmap-Liste waren Fehlannahmen. Eine Analyse mit
anschließender Gegenprüfung (jeder Befund musste einen Widerlegungsversuch
überstehen) hat sie ausgeräumt, bevor Code geändert wurde:
 - "ab Seite 2 alles nach rechts verschoben" existiert nicht. Nachgemessen am
   Prod-PDF ND2026-0015 mit pdftotext -bbox: Seite 1 und Seite 2 beginnen beide
   bei 16,0 mm. Die echten Umbruchfehler waren andere.
 - measure_status validieren war seit Phase 1 erledigt.
 - Werkzeug-IDs / Teilnetz-Gruppierung / TCPDF-Fußzeile: verworfen, die
   vorgeschlagenen Änderungen hätten das PDF verschlechtert.

PDF (alle Punkte am mehrseitigen Dokument nachgeprüft):
- Tabellenkopf der Geräteliste wird auf Folgeseiten wiederholt. Vorher standen
  ab Seite 2 unbeschriftete Spalten — bei leeren MAC/Hostname-Feldern vier
  namenlose Spalten.
- Messungs-Titelzeile und Ergebnis werden zusammengehalten. Vorher blieb die
  Überschrift samt Ampel am Seitenende allein zurück, darunter ein leerer,
  unten offener Rahmen; in einem Testlauf über 61 Umbruchlagen 5-mal (~8 %).
- Spalte "Gerätetyp" hatte 15 mm, ließ aber 10 Zeichen zu — "Chromecast/TV"
  lief bis 199,0 mm bei 195 mm Tabellenkante über den Rahmen in den Druckrand.
- Deutsche Bezeichnungen mit Einheiten statt roher JSON-Schlüssel: aus
  "VerlustProzent: 0 | MinMs: 4.4 | UptimeSek: 8123456" wird "Paketverlust: 0 %
  | Kürzeste Antwortzeit: 4.4 ms | Betriebszeit: 94 Tage 1 Std". Als Whitelist
  (netdiagKundenfelder(), gemeinsam für Karte und PDF) — interne Felder wie
  arpAvailable, mdnsOk, probed, answered fallen damit automatisch heraus.
- "ARP-Tabelle nicht lesbar (/proc/net/arp) — braucht Root" wird beim Drucken
  zu einem kundentauglichen Satz. Altdaten stehen so in der DB, deshalb
  Ersetzung beim Drucken statt nur in der App.

Gerätemerkmale (der eigentliche Roadmap-Punkt): Der Techniker sah in der App
"Drucker HP, Port 9100", im Kundenprotokoll stand nur die IP. Die Felder
fehlten dabei nicht in der Übertragung, sondern durchgängig — ein Fix allein
in der API wäre folgenlos geblieben, weil Dolibarrs setSaveQuery() nur
deklarierte $fields schreibt. Ergänzt über die ganze Kette:
  sql/llx_netdiag_device.sql + neue Migration llx_netdiag_device_v2.sql
  (ADD COLUMN IF NOT EXISTS, wiederholbar, läuft bei jedem Modul-Update),
  NetDiagDevice::$fields + Properties, api/protocols.php POST und GET,
  Kartenansicht und PDF.
Neu: netbios_name, mdns_name, mdns_services, custom_name, open_ports,
found_via, last_seen. Im PDF steht jetzt statt "192.168.178.20" die Zeile
"Brother HL-L2350DW · Brother · Drucker · 80,443,9100" — der Name kommt aus
mDNS, obwohl der Hostname leer ist.

Sprachschlüssel Vendor -> NetDiagVendor: Die Gegenprüfung hielt den Punkt für
falsch (Translate::load() ist first-wins, im CLI-Test kam "Hersteller"), im
Browser stand in der Kartenansicht aber "Lieferant" — im HTTP-Kontext lädt
Dolibarr vorher andere Sprachdateien als im CLI. Statt der Ursache nachzugehen
jetzt ein eigener, kollisionsfreier Schlüssel; im Browser gegengeprüft.

Nebenbei: doppeltes "OK OK" beim Status 0 im PDF.

Gegen das Test-Dolibarr geprüft: Sync über die echte API (Login, POST, GET),
Felder in der DB kontrolliert, Kartenansicht im Browser, mehrseitiges PDF
gerendert und angesehen, Migration zweimal ausgeführt (idempotent).
2026-08-15 18:54:01 +02:00

240 lines
9.8 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/api/protocols.php
* \ingroup netdiag
* \brief API-Endpunkt: Protokolle lesen (GET ?id=) und synchronisieren
* (POST action=sync). Der Sync ist idempotent über client_uuid.
*/
require_once __DIR__.'/netdiag_api.lib.php';
netdiag_api_bootstrap();
/**
* @var Conf $conf
* @var DoliDB $db
* @var Translate $langs
*/
$user = netdiag_api_authenticate($db);
require_once __DIR__.'/../class/netdiagprotocol.class.php';
require_once __DIR__.'/../class/netdiagdevice.class.php';
require_once __DIR__.'/../class/netdiagmeasurement.class.php';
// =========================================================================
// GET: einzelnes Protokoll mit Geräten und Messungen
// =========================================================================
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if ($id <= 0) {
netdiag_api_error('Parameter id fehlt', 400);
}
$protocol = new NetDiagProtocol($db);
if ($protocol->fetch($id) <= 0) {
netdiag_api_error('Protokoll nicht gefunden', 404);
}
$devObj = new NetDiagDevice($db);
$devices = array();
foreach ($devObj->fetchAllByProtocol($protocol->id) as $d) {
$devices[] = array(
'id' => (int) $d->id,
'ip' => $d->ip,
'mac' => $d->mac,
'hostname' => $d->hostname,
'netbiosName' => $d->netbios_name,
'mdnsName' => $d->mdns_name,
'mdnsServices' => netdiag_split_list($d->mdns_services),
'customName' => $d->custom_name,
'vendor' => $d->vendor,
'deviceType' => $d->devicetype,
'openPorts' => array_map('intval', netdiag_split_list($d->open_ports)),
'foundVia' => $d->found_via,
'lastSeen' => !empty($d->last_seen) ? (int) $db->jdate($d->last_seen) * 1000 : null,
'note' => $d->note,
);
}
$measObj = new NetDiagMeasurement($db);
$measurements = array();
foreach ($measObj->fetchAllByProtocol($protocol->id) as $m) {
$measurements[] = array(
'id' => (int) $m->id,
'deviceId' => $m->fk_device ? (int) $m->fk_device : null,
'tool' => $m->tool,
'category' => $m->category,
'label' => $m->label,
'params' => $m->params ? json_decode($m->params, true) : null,
'result' => $m->result ? json_decode($m->result, true) : null,
'measureStatus' => (int) $m->measure_status,
'dateMeasure' => $db->jdate($m->date_measure),
);
}
netdiag_api_respond(array(
'protocol' => array(
'id' => (int) $protocol->id,
'ref' => $protocol->ref,
'label' => $protocol->label,
'clientUuid' => $protocol->client_uuid,
'socId' => $protocol->fk_soc ? (int) $protocol->fk_soc : null,
'orderId' => $protocol->fk_commande ? (int) $protocol->fk_commande : null,
'dateDiag' => $db->jdate($protocol->date_diag),
'location' => $protocol->standort,
'subnet' => $protocol->subnet,
'status' => (int) $protocol->status,
'note' => $protocol->note,
),
'devices' => $devices,
'measurements' => $measurements,
));
}
// =========================================================================
// POST: Protokoll synchronisieren (anlegen oder aktualisieren)
// =========================================================================
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
netdiag_api_error('Methode nicht erlaubt', 405);
}
if (!$user->hasRight('netdiag', 'protocol', 'write')) {
netdiag_api_error('Keine Schreibberechtigung', 403);
}
$body = netdiag_api_read_body();
if (($body['action'] ?? '') !== 'sync' || empty($body['protocol']) || !is_array($body['protocol'])) {
netdiag_api_error('Erwartet: { action: "sync", protocol: {...} }', 400);
}
$p = $body['protocol'];
$uuid = isset($p['clientUuid']) ? trim((string) $p['clientUuid']) : '';
if ($uuid === '') {
netdiag_api_error('protocol.clientUuid erforderlich', 400);
}
$db->begin();
// Vorhandenes Protokoll über UUID suchen (idempotent)
$protocol = new NetDiagProtocol($db);
$exists = $protocol->fetchByClientUuid($uuid);
if ($exists < 0) {
$db->rollback();
netdiag_api_error('Datenbankfehler beim Laden', 500);
}
$protocol->client_uuid = $uuid;
$protocol->label = isset($p['label']) ? (string) $p['label'] : '';
$protocol->fk_soc = !empty($p['socId']) ? (int) $p['socId'] : null;
$protocol->fk_commande = !empty($p['orderId']) ? (int) $p['orderId'] : null;
$protocol->date_diag = netdiag_api_timestamp($p['dateDiag'] ?? 0);
$protocol->fk_user_techniker = (int) $user->id;
$protocol->standort = isset($p['location']) ? (string) $p['location'] : '';
$protocol->subnet = isset($p['subnet']) ? (string) $p['subnet'] : '';
$protocol->status = isset($p['status']) ? (int) $p['status'] : NetDiagProtocol::STATUS_DRAFT;
$protocol->note = isset($p['note']) ? (string) $p['note'] : '';
// tms explizit setzen — die Spalte ist NOT NULL ohne brauchbaren NULL-Default
// (explicit_defaults_for_timestamp=1), createCommon würde sonst NULL einfügen.
$protocol->tms = dol_now();
if ($exists > 0) {
$result = $protocol->update($user, 1);
} else {
$result = $protocol->create($user, 1);
}
if ($result <= 0) {
$db->rollback();
netdiag_api_error('Protokoll speichern fehlgeschlagen: '.$protocol->errorsToString(), 500);
}
$protocolId = (int) $protocol->id;
// Alte Geräte und Messungen entfernen (Sync ersetzt komplett)
$db->query("DELETE FROM ".$db->prefix()."netdiag_measurement WHERE fk_protocol = ".$protocolId);
$db->query("DELETE FROM ".$db->prefix()."netdiag_device WHERE fk_protocol = ".$protocolId);
// Geräte einfügen, dabei clientId -> serverRowid merken
$deviceIdMap = array();
$devicesIn = (!empty($p['devices']) && is_array($p['devices'])) ? $p['devices'] : array();
foreach ($devicesIn as $d) {
$dev = new NetDiagDevice($db);
$dev->fk_protocol = $protocolId;
$dev->ip = isset($d['ip']) ? (string) $d['ip'] : '';
$dev->mac = isset($d['mac']) ? (string) $d['mac'] : '';
$dev->hostname = isset($d['hostname']) ? (string) $d['hostname'] : '';
$dev->vendor = isset($d['vendor']) ? (string) $d['vendor'] : '';
$dev->devicetype = isset($d['deviceType']) ? (string) $d['deviceType'] : '';
$dev->note = isset($d['note']) ? (string) $d['note'] : '';
// Gerätemerkmale aus dem IP-Scan. Ohne sie steht im Kundenprotokoll nur
// die IP, während der Techniker in der App „Drucker HP, Port 9100" sieht.
$dev->netbios_name = isset($d['netbiosName']) ? (string) $d['netbiosName'] : '';
$dev->mdns_name = isset($d['mdnsName']) ? (string) $d['mdnsName'] : '';
$dev->mdns_services = netdiag_join_list($d['mdnsServices'] ?? null, 512);
$dev->custom_name = isset($d['customName']) ? (string) $d['customName'] : '';
$dev->open_ports = netdiag_join_list($d['openPorts'] ?? null, 255);
$dev->found_via = isset($d['foundVia']) ? substr((string) $d['foundVia'], 0, 16) : '';
// lastSeen kommt als Unix-Zeit in Millisekunden aus der App
$dev->last_seen = !empty($d['lastSeen']) ? $db->idate((int) round(((float) $d['lastSeen']) / 1000)) : null;
$dev->tms = dol_now();
if ($dev->create($user, 1) <= 0) {
$db->rollback();
netdiag_api_error('Gerät speichern fehlgeschlagen: '.$dev->errorsToString(), 500);
}
if (isset($d['clientId'])) {
$deviceIdMap[(string) $d['clientId']] = (int) $dev->id;
}
}
// Messungen einfügen, deviceClientId auf serverRowid abbilden
$measIn = (!empty($p['measurements']) && is_array($p['measurements'])) ? $p['measurements'] : array();
foreach ($measIn as $m) {
$meas = new NetDiagMeasurement($db);
$meas->fk_protocol = $protocolId;
$dcid = isset($m['deviceClientId']) ? (string) $m['deviceClientId'] : '';
$meas->fk_device = ($dcid !== '' && isset($deviceIdMap[$dcid])) ? $deviceIdMap[$dcid] : null;
$meas->tool = isset($m['tool']) ? (string) $m['tool'] : 'unknown';
$meas->category = isset($m['category']) ? (string) $m['category'] : '';
$meas->label = isset($m['label']) ? (string) $m['label'] : '';
$meas->params = isset($m['params']) ? json_encode($m['params'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
$meas->result = isset($m['result']) ? json_encode($m['result'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
// Nur bekannte Bewertungen uebernehmen. Ein unbekannter Wert landete sonst
// als Array-Index in Karte und PDF und lief dort ins Leere.
$st = isset($m['measureStatus']) ? (int) $m['measureStatus'] : 0;
$meas->measure_status = ($st >= 0 && $st <= 3) ? $st : 1;
$meas->date_measure = netdiag_api_timestamp($m['dateMeasure'] ?? 0);
$meas->tms = dol_now();
if ($meas->create($user, 1) <= 0) {
$db->rollback();
netdiag_api_error('Messung speichern fehlgeschlagen: '.$meas->errorsToString(), 500);
}
}
$db->commit();
// PDF neu erzeugen, wenn das Protokoll abgeschlossen ist
$pdfgenerated = false;
if ($protocol->status == NetDiagProtocol::STATUS_DONE) {
require_once __DIR__.'/../lib/netdiag_pdf.lib.php';
$pdfgenerated = (netdiagGeneratePdf($db, $protocol, $langs) > 0);
}
netdiag_api_respond(array(
'ok' => true,
'protocolId' => $protocolId,
'ref' => $protocol->ref,
'created' => ($exists == 0),
'pdfGenerated' => $pdfgenerated,
));