Initiales Commit — Dolibarr-Modul NetDiag [deploy]
Some checks are pending
Deploy netdiag / deploy (push) Waiting to run

Netzwerk-Diagnose-Modul mit JSON-API für die NetDiag-App:
- 3 Tabellen (protocol/device/measurement), generisches JSON-result
- JSON-API: auth, customers, orders, protocols (idempotenter Sync), pdf
- JWT-Auth (HS256), CORS für die Capacitor-App
- Tabs an Thirdparty + Auftrag, Protokoll-Card, PDF-Generator
- QR-Code zum App-Download in der Modul-Konfiguration
- de_DE + en_US, Rechtesystem netdiag->protocol read/write/delete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-05-19 12:12:11 +02:00
commit c576726a26
28 changed files with 3213 additions and 0 deletions

View file

@ -0,0 +1,70 @@
name: Deploy netdiag
on:
push:
tags:
- 'v*'
branches:
- main
jobs:
deploy:
runs-on: docker
if: startsWith(github.ref, 'refs/tags/v') || contains(github.event.head_commit.message, '[deploy]')
steps:
- name: Notify Start
uses: https://git.data-it-solution.de/data/ntfy-action@main
with:
status: start
project: NetDiag
ntfy_auth: ${{ secrets.NTFY_AUTH }}
run_number: ${{ github.run_number }}
message: ${{ github.event.head_commit.message }}
- name: Checkout
run: |
git clone --depth 1 --branch "${GITHUB_REF_NAME}" \
"https://token:${{ secrets.GIT_TOKEN }}@git.data-it-solution.de/${GITHUB_REPOSITORY}.git" .
- name: Deploy nach Dolibarr
run: |
DEPLOY_PATH="/mnt/appdata/firma/dolibarr-202509/modules/netdiag"
REF="${GITHUB_REF#refs/*/}"
echo "Deploye ${REF} nach ${DEPLOY_PATH} ..."
if [ -d "$DEPLOY_PATH" ]; then
find "$DEPLOY_PATH" -mindepth 1 -not -path '*/.git/*' -not -name '.git' -delete 2>/dev/null || true
else
mkdir -p "$DEPLOY_PATH"
fi
rsync -a \
--exclude='.git' \
--exclude='.forgejo' \
--exclude='.gitignore' \
--exclude='CLAUDE.md' \
--exclude='test/' \
./ "$DEPLOY_PATH/"
echo "Deployment erfolgreich: ${REF} -> ${DEPLOY_PATH}"
ls -la "$DEPLOY_PATH/core/modules/"
- name: Notify Success
if: success()
uses: https://git.data-it-solution.de/data/ntfy-action@main
with:
status: success
project: NetDiag
ntfy_auth: ${{ secrets.NTFY_AUTH }}
run_number: ${{ github.run_number }}
- name: Notify Failure
if: failure()
uses: https://git.data-it-solution.de/data/ntfy-action@main
with:
status: failure
project: NetDiag
ntfy_auth: ${{ secrets.NTFY_AUTH }}
run_number: ${{ github.run_number }}
click_url: https://git.data-it-solution.de/${{ github.repository }}/actions

11
ChangeLog.md Normal file
View file

@ -0,0 +1,11 @@
# Changelog NetDiag
## 1.0.0
- Erstversion
- Datenmodell: Protokoll, Gerät, Messung
- JSON-API: auth, customers, orders, protocols (Sync), pdf
- Tab "Netzwerk-Diagnose" an Kunde und Auftrag
- PDF-Protokoll-Generator + ECM-Ablage
- Backend: Protokoll-Liste und Detailansicht
- Admin: Token-Einstellung, QR-Code zum App-Download

49
README.md Normal file
View file

@ -0,0 +1,49 @@
# NetDiag — Netzwerk-Diagnose für Dolibarr
Dolibarr-Modul für die Ablage von Netzwerk-Diagnose-Protokollen. Erfasst per
mobiler App (siehe Projekt `NetzwerkDiagnose/app`) gefundene Geräte, Ports und
Messungen und hängt die Protokolle dauerhaft an **Kunde** und **Auftrag**.
## Funktionen
- Datenmodell: Protokoll → Geräte → Messungen (`llx_netdiag_*`)
- Tab **Netzwerk-Diagnose** an Kunde (thirdparty) und Auftrag (commande)
- JSON-API unter `/custom/netdiag/api/` für die mobile App (JWT-Auth)
- PDF-Protokoll, wird im Dokumentenarchiv (ECM) abgelegt
- Rechtesystem: `netdiag → protocol → read/write/delete`
- Mehrsprachig (de_DE, en_US)
- QR-Code zum App-Download in der Modul-Einrichtung
## Installation
1. Verzeichnis `netdiag/` nach `htdocs/custom/` auf den Dolibarr-Server kopieren.
2. In Dolibarr: **Einrichtung → Module → NetDiag** aktivieren.
3. Beim Aktivieren werden die Tabellen `llx_netdiag_protocol`,
`llx_netdiag_device`, `llx_netdiag_measurement` angelegt und ein
JWT-Schlüssel erzeugt.
4. Benutzern das Recht **NetDiag → Protokolle lesen/schreiben** geben.
## API-Endpunkte
Alle unter `https://<dolibarr>/custom/netdiag/api/`:
| Endpunkt | Methode | Zweck |
|----------|---------|-------|
| `auth.php` | POST `{login,password}` | Anmeldung → `{token,expiresIn,user}` |
| `customers.php` | GET `?q=` / `?id=` | Kundensuche / Kundendetail |
| `orders.php` | GET `?open=1&q=` / `?id=` | Auftragsliste / Auftragsdetail |
| `protocols.php` | GET `?id=` | Protokoll mit Geräten + Messungen |
| `protocols.php` | POST `{action:"sync",protocol:{…}}` | Protokoll anlegen/aktualisieren (idempotent über `clientUuid`) |
| `pdf.php` | GET `?id=&jwt=` | Protokoll-PDF streamen |
Authentifizierung per `Authorization: Bearer <token>` oder `?jwt=<token>`.
## Einrichtung
**Einrichtung → Module → NetDiag → Einstellungen:**
- Token-Gültigkeit (Sekunden)
- App-Download-URL (APK) — wird als QR-Code angezeigt
## Lizenz
GPLv3

87
admin/about.php Normal file
View file

@ -0,0 +1,87 @@
<?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/admin/about.php
* \ingroup netdiag
* \brief Info-Seite des Moduls NetDiag
*/
// 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."/core/lib/admin.lib.php";
require_once '../lib/netdiag.lib.php';
/**
* @var DoliDB $db
* @var Translate $langs
* @var User $user
*/
$langs->loadLangs(array("admin", "netdiag@netdiag"));
if (!$user->admin) {
accessforbidden();
}
$title = $langs->trans("NetDiagAbout");
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-netdiag page-admin');
$linkback = '<a href="'.DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1">'.$langs->trans("BackToModuleList").'</a>';
print load_fiche_titre($title, $linkback, 'title_setup');
$head = netdiagAdminPrepareHead();
print dol_get_fiche_head($head, 'about', $langs->trans("ModuleNetDiagName"), -1, 'fa-network-wired');
print '<div class="paddingtop paddingbottom">';
print '<strong>'.$langs->trans("ModuleNetDiagName").'</strong> &mdash; '.$langs->trans("ModuleNetDiagDesc").'<br><br>';
print 'Version: 1.0.0<br>';
print 'Autor: Alles Watt läuft (Eduard Wisch)<br>';
print 'Lizenz: GPLv3<br>';
print '</div>';
print dol_get_fiche_end();
llxFooter();
$db->close();

147
admin/setup.php Normal file
View file

@ -0,0 +1,147 @@
<?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/admin/setup.php
* \ingroup netdiag
* \brief Einrichtungsseite des Moduls NetDiag
*/
// 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."/core/lib/admin.lib.php";
require_once '../lib/netdiag.lib.php';
/**
* @var Conf $conf
* @var DoliDB $db
* @var Translate $langs
* @var User $user
*/
$langs->loadLangs(array("admin", "netdiag@netdiag"));
if (!$user->admin) {
accessforbidden();
}
$action = GETPOST('action', 'aZ09');
// Konstanten speichern
if ($action == 'updateconst') {
$ttl = GETPOSTINT('NETDIAG_API_TOKEN_TTL');
if ($ttl < 60) {
$ttl = 604800;
}
dolibarr_set_const($db, 'NETDIAG_API_TOKEN_TTL', $ttl, 'chaine', 0, '', $conf->entity);
dolibarr_set_const($db, 'NETDIAG_APK_URL', GETPOST('NETDIAG_APK_URL', 'alpha'), 'chaine', 0, '', $conf->entity);
setEventMessages($langs->trans("SetupSaved"), null, 'mesgs');
header("Location: ".$_SERVER["PHP_SELF"]);
exit;
}
/*
* Ansicht
*/
$form = new Form($db);
$help_url = '';
$title = $langs->trans("NetDiagSetup");
llxHeader('', $title, $help_url, '', 0, 0, '', '', '', 'mod-netdiag page-admin');
$linkback = '<a href="'.($backtopage ? $backtopage : DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1').'">'.$langs->trans("BackToModuleList").'</a>';
print load_fiche_titre($title, $linkback, 'title_setup');
$head = netdiagAdminPrepareHead();
print dol_get_fiche_head($head, 'settings', $langs->trans("ModuleNetDiagName"), -1, 'fa-network-wired');
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="updateconst">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre"><td>'.$langs->trans("Parameter").'</td><td>'.$langs->trans("Value").'</td></tr>';
print '<tr class="oddeven"><td>'.$langs->trans("NETDIAG_API_TOKEN_TTL");
print ' '.$form->textwithpicto('', $langs->trans("NETDIAG_API_TOKEN_TTLTooltip")).'</td>';
print '<td><input type="number" name="NETDIAG_API_TOKEN_TTL" value="'.dol_escape_htmltag(getDolGlobalString('NETDIAG_API_TOKEN_TTL', '604800')).'" min="60" class="width150"></td></tr>';
print '<tr class="oddeven"><td>'.$langs->trans("NETDIAG_APK_URL");
print ' '.$form->textwithpicto('', $langs->trans("NETDIAG_APK_URLTooltip")).'</td>';
print '<td><input type="text" name="NETDIAG_APK_URL" value="'.dol_escape_htmltag(getDolGlobalString('NETDIAG_APK_URL')).'" class="minwidth400 quatrevingtpercent"></td></tr>';
print '</table>';
print '<div class="center"><input type="submit" class="button" value="'.$langs->trans("Save").'"></div>';
print '</form>';
print '<br><div class="opacitymedium">'.$langs->trans("NetDiagApiSecretInfo").'</div>';
// QR-Code zum App-Download
$apkurl = getDolGlobalString('NETDIAG_APK_URL');
if (!empty($apkurl)) {
print '<div class="center" style="margin-top:25px;padding:20px;border:1px solid #ddd;border-radius:8px;display:inline-block;">';
print '<strong>'.$langs->trans("NetDiagAppDownload").'</strong><br>';
print '<div class="opacitymedium" style="margin:6px 0 12px 0;">'.$langs->trans("NetDiagAppDownloadHint").'</div>';
$qrfile = DOL_DOCUMENT_ROOT.'/includes/tecnickcom/tcpdf/tcpdf_barcodes_2d.php';
if (file_exists($qrfile)) {
require_once $qrfile;
$barcode = new TCPDF2DBarcode($apkurl, 'QRCODE,M');
// SVG-Ausgabe -> komplett lokal, kein externer Dienst
print '<div style="width:220px;height:220px;margin:0 auto;">';
print $barcode->getBarcodeSVGcode(5, 5, 'black');
print '</div>';
} else {
print '<div class="warning">QR-Bibliothek (TCPDF) nicht gefunden.</div>';
}
print '<div style="margin-top:10px;"><a href="'.dol_escape_htmltag($apkurl).'" target="_blank">'.dol_escape_htmltag($apkurl).'</a></div>';
print '</div>';
}
print dol_get_fiche_end();
llxFooter();
$db->close();

91
api/auth.php Normal file
View file

@ -0,0 +1,91 @@
<?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/auth.php
* \ingroup netdiag
* \brief API-Endpunkt: Anmeldung der mobilen App, liefert JWT.
*
* POST {login, password} -> {token, expiresIn, user}
*/
require_once __DIR__.'/netdiag_api.lib.php';
netdiag_api_bootstrap();
/** @var DoliDB $db */
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
netdiag_api_error('Nur POST erlaubt', 405);
}
$body = netdiag_api_read_body();
$login = isset($body['login']) ? trim((string) $body['login']) : '';
$password = isset($body['password']) ? (string) $body['password'] : '';
if ($login === '' || $password === '') {
netdiag_api_error('Login und Passwort erforderlich', 400);
}
require_once DOL_DOCUMENT_ROOT.'/core/lib/security2.lib.php';
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
// Zugangsdaten gegen Dolibarr prüfen (Standard-Login-Backend)
$entitytocheck = (int) $conf->entity;
$authmode = (getDolGlobalString('MAIN_AUTHENTICATION_MODE') ? getDolGlobalString('MAIN_AUTHENTICATION_MODE') : 'dolibarr');
$resultlogin = checkLoginPassEntity($login, $password, $entitytocheck, explode(',', $authmode));
if (empty($resultlogin)) {
// Kurze Verzögerung gegen Brute-Force
sleep(1);
netdiag_api_error('Login fehlgeschlagen', 401);
}
$user = new User($db);
if ($user->fetch('', $resultlogin, '', 0, $entitytocheck) <= 0 || empty($user->id)) {
netdiag_api_error('Benutzer nicht gefunden', 401);
}
if (!empty($user->statut) && $user->statut == 0) {
netdiag_api_error('Benutzer deaktiviert', 403);
}
$user->loadRights();
if (!$user->hasRight('netdiag', 'protocol', 'read')) {
netdiag_api_error('Keine Berechtigung für NetDiag', 403);
}
$ttl = (int) getDolGlobalString('NETDIAG_API_TOKEN_TTL', '604800');
if ($ttl < 60) {
$ttl = 604800;
}
$token = netdiag_jwt_encode(array(
'sub' => (int) $user->id,
'name' => $user->getFullName($langs),
), $ttl);
netdiag_api_respond(array(
'token' => $token,
'expiresIn' => $ttl,
'user' => array(
'id' => (int) $user->id,
'login' => $user->login,
'name' => $user->getFullName($langs),
'email' => $user->email,
'canWrite' => (bool) $user->hasRight('netdiag', 'protocol', 'write'),
),
));

119
api/customers.php Normal file
View file

@ -0,0 +1,119 @@
<?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/customers.php
* \ingroup netdiag
* \brief API-Endpunkt: Kunden suchen (?q=) oder Kundendetail (?id=).
*/
require_once __DIR__.'/netdiag_api.lib.php';
netdiag_api_bootstrap();
/** @var DoliDB $db */
$user = netdiag_api_authenticate($db);
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$q = isset($_GET['q']) ? trim((string) $_GET['q']) : '';
$limit = isset($_GET['limit']) ? min(200, max(1, (int) $_GET['limit'])) : 50;
$prefix = $db->prefix();
// ---- Kundendetail ----
if ($id > 0) {
$sql = "SELECT s.rowid, s.nom, s.code_client, s.address, s.zip, s.town, s.phone, s.email, s.tva_intra";
$sql .= " FROM ".$prefix."societe as s";
$sql .= " WHERE s.rowid = ".((int) $id);
$sql .= " AND s.entity IN (".getEntity('societe').")";
$resql = $db->query($sql);
if (!$resql || !($obj = $db->fetch_object($resql))) {
netdiag_api_error('Kunde nicht gefunden', 404);
}
$customer = array(
'id' => (int) $obj->rowid,
'name' => $obj->nom,
'code' => $obj->code_client,
'address' => $obj->address,
'zip' => $obj->zip,
'town' => $obj->town,
'phone' => $obj->phone,
'email' => $obj->email,
'vat' => $obj->tva_intra,
);
// Aufträge des Kunden
$orders = array();
$sqlo = "SELECT rowid, ref, ref_client, date_commande, fk_statut FROM ".$prefix."commande";
$sqlo .= " WHERE fk_soc = ".((int) $id)." AND entity IN (".getEntity('commande').")";
$sqlo .= " ORDER BY date_commande DESC, rowid DESC";
$reso = $db->query($sqlo);
if ($reso) {
while ($o = $db->fetch_object($reso)) {
$orders[] = array(
'id' => (int) $o->rowid,
'ref' => $o->ref,
'refClient' => $o->ref_client,
'date' => $db->jdate($o->date_commande),
'status' => (int) $o->fk_statut,
'open' => ((int) $o->fk_statut >= 0 && (int) $o->fk_statut < 3),
);
}
}
// Diagnose-Protokolle des Kunden
$protocols = netdiag_api_protocol_list($db, ' AND p.fk_soc = '.((int) $id));
netdiag_api_respond(array(
'customer' => $customer,
'orders' => $orders,
'protocols' => $protocols,
));
}
// ---- Kundensuche ----
$sql = "SELECT s.rowid, s.nom, s.code_client, s.zip, s.town, s.phone,";
$sql .= " (SELECT COUNT(*) FROM ".$prefix."netdiag_protocol p WHERE p.fk_soc = s.rowid) as protocolcount";
$sql .= " FROM ".$prefix."societe as s";
$sql .= " WHERE s.entity IN (".getEntity('societe').")";
$sql .= " AND s.client IN (1, 2, 3)";
$sql .= " AND s.status = 1";
if ($q !== '') {
$sql .= " AND (".natural_search(array('s.nom', 's.name_alias', 's.code_client', 's.town', 's.zip'), $q, 0, 1).")";
}
$sql .= " ORDER BY s.nom ASC";
$sql .= $db->plimit($limit, 0);
$resql = $db->query($sql);
if (!$resql) {
netdiag_api_error('Datenbankfehler: '.$db->lasterror(), 500);
}
$customers = array();
while ($obj = $db->fetch_object($resql)) {
$customers[] = array(
'id' => (int) $obj->rowid,
'name' => $obj->nom,
'code' => $obj->code_client,
'zip' => $obj->zip,
'town' => $obj->town,
'phone' => $obj->phone,
'protocolCount' => (int) $obj->protocolcount,
);
}
netdiag_api_respond(array('customers' => $customers));

330
api/netdiag_api.lib.php Normal file
View file

@ -0,0 +1,330 @@
<?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/netdiag_api.lib.php
* \ingroup netdiag
* \brief Gemeinsame Funktionen der JSON-API: Bootstrap, JWT, Antworten.
*
* Wird von jedem API-Endpunkt eingebunden. Lädt die Dolibarr-Umgebung
* ohne Web-Session und authentifiziert die mobile App per JWT.
*/
// Konstanten setzen BEVOR Dolibarr geladen wird (kein Menü, kein HTML, kein Login)
if (!defined('NOLOGIN')) {
define('NOLOGIN', '1');
}
if (!defined('NOCSRFCHECK')) {
define('NOCSRFCHECK', '1');
}
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '1');
}
if (!defined('NOREQUIRESOC')) {
define('NOREQUIRESOC', '1');
}
/**
* Dolibarr-Umgebung laden (master.inc.php) und CORS-Header setzen.
*
* @return void
*/
function netdiag_api_bootstrap()
{
// master.inc.php aus dem Webroot finden
$res = 0;
$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))."/master.inc.php")) {
$res = @include substr($tmp, 0, ($i + 1))."/master.inc.php";
}
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/master.inc.php")) {
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/master.inc.php";
}
if (!$res && file_exists("../../../master.inc.php")) {
$res = @include "../../../master.inc.php";
}
if (!$res && file_exists("../../../../master.inc.php")) {
$res = @include "../../../../master.inc.php";
}
if (!$res) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(500);
echo json_encode(array('error' => 'Dolibarr environment not found'));
exit;
}
// CORS: Bearer-Token-Auth, daher Wildcard-Origin erlaubt (keine Cookies)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 86400');
// Preflight sofort beantworten
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
}
/**
* JSON-Antwort senden und Skript beenden.
*
* @param mixed $data Antwortdaten
* @param int $httpstatus HTTP-Statuscode
* @return void
*/
function netdiag_api_respond($data, $httpstatus = 200)
{
header('Content-Type: application/json; charset=utf-8');
http_response_code($httpstatus);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
/**
* Fehler-Antwort senden und Skript beenden.
*
* @param string $message Fehlermeldung
* @param int $httpstatus HTTP-Statuscode
* @return void
*/
function netdiag_api_error($message, $httpstatus = 400)
{
netdiag_api_respond(array('error' => $message), $httpstatus);
}
/**
* Base64-URL-kodieren (JWT-konform, ohne Padding).
*
* @param string $data Rohdaten
* @return string Kodierter String
*/
function netdiag_base64url_encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* Base64-URL-dekodieren.
*
* @param string $data Kodierter String
* @return string Rohdaten
*/
function netdiag_base64url_decode($data)
{
return base64_decode(strtr($data, '-_', '+/'));
}
/**
* Geheimen JWT-Schlüssel des Moduls holen.
*
* @return string Schlüssel
*/
function netdiag_jwt_secret()
{
$secret = getDolGlobalString('NETDIAG_API_JWT_SECRET');
if (empty($secret)) {
// Fallback: Instanz-eindeutiger Wert (sollte nach Modulaktivierung nicht eintreten)
$secret = md5(DOL_DOCUMENT_ROOT.getDolGlobalString('MAIN_INFO_SOCIETE_NOM'));
}
return $secret;
}
/**
* JWT (HS256) erzeugen.
*
* @param array<string,mixed> $payload Nutzdaten (sub, name, exp werden ergänzt)
* @param int $ttl Gültigkeit in Sekunden
* @return string Signiertes Token
*/
function netdiag_jwt_encode($payload, $ttl)
{
$header = array('alg' => 'HS256', 'typ' => 'JWT');
$now = dol_now();
$payload['iat'] = $now;
$payload['exp'] = $now + $ttl;
$seg = array();
$seg[] = netdiag_base64url_encode(json_encode($header));
$seg[] = netdiag_base64url_encode(json_encode($payload));
$signinginput = implode('.', $seg);
$signature = hash_hmac('sha256', $signinginput, netdiag_jwt_secret(), true);
$seg[] = netdiag_base64url_encode($signature);
return implode('.', $seg);
}
/**
* JWT prüfen und Nutzdaten zurückgeben.
*
* @param string $token JWT
* @return array<string,mixed>|null Nutzdaten oder null bei ungültig/abgelaufen
*/
function netdiag_jwt_decode($token)
{
$parts = explode('.', (string) $token);
if (count($parts) !== 3) {
return null;
}
list($h, $p, $s) = $parts;
$expected = hash_hmac('sha256', $h.'.'.$p, netdiag_jwt_secret(), true);
$given = netdiag_base64url_decode($s);
if (!hash_equals($expected, $given)) {
return null;
}
$payload = json_decode(netdiag_base64url_decode($p), true);
if (!is_array($payload)) {
return null;
}
if (empty($payload['exp']) || $payload['exp'] < dol_now()) {
return null;
}
return $payload;
}
/**
* Token aus Request lesen (Authorization-Header oder ?jwt=).
*
* @return string Token oder leerer String
*/
function netdiag_api_read_token()
{
$auth = '';
if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
$auth = $_SERVER['HTTP_AUTHORIZATION'];
} elseif (!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$auth = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
if (!empty($headers['Authorization'])) {
$auth = $headers['Authorization'];
}
}
if (stripos($auth, 'Bearer ') === 0) {
return trim(substr($auth, 7));
}
if (!empty($_GET['jwt'])) {
return (string) $_GET['jwt'];
}
return '';
}
/**
* Aktuellen Request authentifizieren. Bricht mit 401 ab, wenn ungültig.
*
* @param DoliDB $db Datenbank-Handler
* @return User Geladenes Benutzer-Objekt
*/
function netdiag_api_authenticate($db)
{
$token = netdiag_api_read_token();
if (empty($token)) {
netdiag_api_error('Kein Token übermittelt', 401);
}
$payload = netdiag_jwt_decode($token);
if ($payload === null || empty($payload['sub'])) {
netdiag_api_error('Token ungültig oder abgelaufen', 401);
}
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
$user = new User($db);
if ($user->fetch((int) $payload['sub']) <= 0 || empty($user->id)) {
netdiag_api_error('Benutzer nicht gefunden', 401);
}
if (!empty($user->statut) && $user->statut == 0) {
netdiag_api_error('Benutzer deaktiviert', 403);
}
$user->loadRights();
if (!$user->hasRight('netdiag', 'protocol', 'read')) {
netdiag_api_error('Keine Berechtigung für NetDiag', 403);
}
return $user;
}
/**
* JSON-Body eines POST-Requests einlesen.
*
* @return array<string,mixed> Dekodierte Daten (leer bei Fehler)
*/
function netdiag_api_read_body()
{
$raw = file_get_contents('php://input');
if (empty($raw)) {
return array();
}
$data = json_decode($raw, true);
return is_array($data) ? $data : array();
}
/**
* Liste von Diagnose-Protokollen als Array zurückgeben (für API-Antworten).
*
* @param DoliDB $db Datenbank-Handler
* @param string $filtersql Zusätzlicher SQL-Filter, beginnend mit ' AND ...'
* @return array<int,array<string,mixed>> Liste der Protokolle
*/
function netdiag_api_protocol_list($db, $filtersql = '')
{
$prefix = $db->prefix();
$sql = "SELECT p.rowid, p.ref, p.label, p.client_uuid, p.fk_soc, p.fk_commande,";
$sql .= " p.date_diag, p.standort, p.subnet, p.status,";
$sql .= " (SELECT COUNT(*) FROM ".$prefix."netdiag_device d WHERE d.fk_protocol = p.rowid) as devcount,";
$sql .= " (SELECT COUNT(*) FROM ".$prefix."netdiag_measurement m WHERE m.fk_protocol = p.rowid) as meascount";
$sql .= " FROM ".$prefix."netdiag_protocol as p";
$sql .= " WHERE p.entity IN (".getEntity('netdiagprotocol').")";
$sql .= $filtersql;
$sql .= " ORDER BY p.date_diag DESC, p.rowid DESC";
$list = array();
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$list[] = array(
'id' => (int) $obj->rowid,
'ref' => $obj->ref,
'label' => $obj->label,
'clientUuid' => $obj->client_uuid,
'socId' => $obj->fk_soc ? (int) $obj->fk_soc : null,
'orderId' => $obj->fk_commande ? (int) $obj->fk_commande : null,
'dateDiag' => $db->jdate($obj->date_diag),
'location' => $obj->standort,
'subnet' => $obj->subnet,
'status' => (int) $obj->status,
'deviceCount' => (int) $obj->devcount,
'measureCount' => (int) $obj->meascount,
);
}
}
return $list;
}

117
api/orders.php Normal file
View file

@ -0,0 +1,117 @@
<?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/orders.php
* \ingroup netdiag
* \brief API-Endpunkt: Aufträge listen (?open=1&q=) oder Auftragsdetail (?id=).
*
* Auftragsstatus (Dolibarr commande.fk_statut):
* -1 = storniert, 0 = Entwurf, 1 = validiert, 2 = in Bearbeitung,
* 3 = abgeschlossen. "Aktiv" = Status 0/1/2.
*/
require_once __DIR__.'/netdiag_api.lib.php';
netdiag_api_bootstrap();
/** @var DoliDB $db */
$user = netdiag_api_authenticate($db);
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$q = isset($_GET['q']) ? trim((string) $_GET['q']) : '';
$onlyopen = !empty($_GET['open']);
$limit = isset($_GET['limit']) ? min(200, max(1, (int) $_GET['limit'])) : 80;
$prefix = $db->prefix();
// ---- Auftragsdetail ----
if ($id > 0) {
$sql = "SELECT c.rowid, c.ref, c.ref_client, c.date_commande, c.fk_statut, c.note_public,";
$sql .= " s.rowid as socid, s.nom as socname, s.address, s.zip, s.town, s.phone, s.email";
$sql .= " FROM ".$prefix."commande as c";
$sql .= " LEFT JOIN ".$prefix."societe as s ON s.rowid = c.fk_soc";
$sql .= " WHERE c.rowid = ".((int) $id)." AND c.entity IN (".getEntity('commande').")";
$resql = $db->query($sql);
if (!$resql || !($obj = $db->fetch_object($resql))) {
netdiag_api_error('Auftrag nicht gefunden', 404);
}
$order = array(
'id' => (int) $obj->rowid,
'ref' => $obj->ref,
'refClient' => $obj->ref_client,
'date' => $db->jdate($obj->date_commande),
'status' => (int) $obj->fk_statut,
'open' => ((int) $obj->fk_statut >= 0 && (int) $obj->fk_statut < 3),
'note' => $obj->note_public,
'customer' => array(
'id' => (int) $obj->socid,
'name' => $obj->socname,
'address' => $obj->address,
'zip' => $obj->zip,
'town' => $obj->town,
'phone' => $obj->phone,
'email' => $obj->email,
),
);
$protocols = netdiag_api_protocol_list($db, ' AND p.fk_commande = '.((int) $id));
netdiag_api_respond(array('order' => $order, 'protocols' => $protocols));
}
// ---- Auftragsliste ----
$sql = "SELECT c.rowid, c.ref, c.ref_client, c.date_commande, c.fk_statut,";
$sql .= " s.rowid as socid, s.nom as socname, s.zip, s.town,";
$sql .= " (SELECT COUNT(*) FROM ".$prefix."netdiag_protocol p WHERE p.fk_commande = c.rowid) as protocolcount";
$sql .= " FROM ".$prefix."commande as c";
$sql .= " LEFT JOIN ".$prefix."societe as s ON s.rowid = c.fk_soc";
$sql .= " WHERE c.entity IN (".getEntity('commande').")";
if ($onlyopen) {
// Aktive Aufträge: Entwurf / validiert / in Bearbeitung
$sql .= " AND c.fk_statut IN (0, 1, 2)";
}
if ($q !== '') {
$sql .= " AND (".natural_search(array('c.ref', 'c.ref_client', 's.nom', 's.town'), $q, 0, 1).")";
}
$sql .= " ORDER BY c.date_commande DESC, c.rowid DESC";
$sql .= $db->plimit($limit, 0);
$resql = $db->query($sql);
if (!$resql) {
netdiag_api_error('Datenbankfehler: '.$db->lasterror(), 500);
}
$orders = array();
while ($obj = $db->fetch_object($resql)) {
$orders[] = array(
'id' => (int) $obj->rowid,
'ref' => $obj->ref,
'refClient' => $obj->ref_client,
'date' => $db->jdate($obj->date_commande),
'status' => (int) $obj->fk_statut,
'open' => ((int) $obj->fk_statut >= 0 && (int) $obj->fk_statut < 3),
'protocolCount' => (int) $obj->protocolcount,
'customer' => array(
'id' => $obj->socid ? (int) $obj->socid : null,
'name' => $obj->socname,
'zip' => $obj->zip,
'town' => $obj->town,
),
);
}
netdiag_api_respond(array('orders' => $orders));

67
api/pdf.php Normal file
View file

@ -0,0 +1,67 @@
<?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/pdf.php
* \ingroup netdiag
* \brief API-Endpunkt: Protokoll-PDF streamen (GET ?id=&jwt=).
*/
require_once __DIR__.'/netdiag_api.lib.php';
netdiag_api_bootstrap();
/**
* @var DoliDB $db
* @var Translate $langs
*/
$user = netdiag_api_authenticate($db);
require_once __DIR__.'/../class/netdiagprotocol.class.php';
require_once __DIR__.'/../lib/netdiag.lib.php';
require_once __DIR__.'/../lib/netdiag_pdf.lib.php';
$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);
}
$file = netdiagGetOutputDir().'/'.dol_sanitizeFileName($protocol->ref).'/'.dol_sanitizeFileName($protocol->ref).'.pdf';
// PDF erzeugen, wenn noch nicht vorhanden oder Neuerzeugung verlangt
if (!dol_is_file($file) || !empty($_GET['regenerate'])) {
if (netdiagGeneratePdf($db, $protocol, $langs) <= 0) {
netdiag_api_error('PDF-Erzeugung fehlgeschlagen', 500);
}
}
if (!dol_is_file($file)) {
netdiag_api_error('PDF nicht gefunden', 404);
}
// PDF ausliefern
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="'.dol_sanitizeFileName($protocol->ref).'.pdf"');
header('Content-Length: '.dol_filesize($file));
header('Cache-Control: private, max-age=0');
readfile($file);
exit;

215
api/protocols.php Normal file
View file

@ -0,0 +1,215 @@
<?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,
'vendor' => $d->vendor,
'deviceType' => $d->devicetype,
'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 = !empty($p['dateDiag']) ? (int) $p['dateDiag'] : dol_now();
$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'] : '';
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->error, 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'] : '';
if ($dev->create($user, 1) <= 0) {
$db->rollback();
netdiag_api_error('Gerät speichern fehlgeschlagen: '.$dev->error, 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;
$meas->measure_status = isset($m['measureStatus']) ? (int) $m['measureStatus'] : 0;
$meas->date_measure = !empty($m['dateMeasure']) ? (int) $m['dateMeasure'] : dol_now();
if ($meas->create($user, 1) <= 0) {
$db->rollback();
netdiag_api_error('Messung speichern fehlgeschlagen: '.$meas->error, 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,
));

View file

@ -0,0 +1,156 @@
<?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 htdocs/custom/netdiag/class/netdiagdevice.class.php
* \ingroup netdiag
* \brief Klasse für ein im Diagnose-Protokoll gefundenes Gerät
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
/**
* Klasse NetDiagDevice ein im Netzwerk gefundenes Gerät.
*/
class NetDiagDevice extends CommonObject
{
/** @var string Modul-Name */
public $module = 'netdiag';
/** @var string Element-Typ */
public $element = 'netdiagdevice';
/** @var string Datenbanktabelle (ohne Präfix) */
public $table_element = 'netdiag_device';
/** @var string Icon */
public $picto = 'fa-desktop';
/** @var int 1 = Tabelle hat ein 'entity'-Feld */
public $ismultientitymanaged = 1;
/**
* @var array<string,array<string,mixed>> Felddefinitionen
*/
public $fields = array(
'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'index' => 1, 'position' => 1),
'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'default' => 1, 'index' => 1, 'position' => 5),
'fk_protocol' => array('type' => 'integer', 'label' => 'Protocol', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'index' => 1, 'position' => 10),
'ip' => array('type' => 'varchar(45)', 'label' => 'IpAddress', 'enabled' => 1, 'visible' => 1, 'position' => 20),
'mac' => array('type' => 'varchar(17)', 'label' => 'MacAddress', 'enabled' => 1, 'visible' => 1, 'position' => 25),
'hostname' => array('type' => 'varchar(255)', 'label' => 'Hostname', 'enabled' => 1, 'visible' => 1, 'position' => 30),
'vendor' => array('type' => 'varchar(128)', 'label' => 'Vendor', 'enabled' => 1, 'visible' => 1, 'position' => 35),
'devicetype' => array('type' => 'varchar(64)', 'label' => 'DeviceType', 'enabled' => 1, 'visible' => 1, 'position' => 40),
'note' => array('type' => 'text', 'label' => 'Note', 'enabled' => 1, 'visible' => 3, 'position' => 50),
'date_creation' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'position' => 500),
'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'position' => 501),
);
public $rowid;
public $entity;
public $fk_protocol;
public $ip;
public $mac;
public $hostname;
public $vendor;
public $devicetype;
public $note;
public $date_creation;
public $tms;
/**
* Konstruktor
*
* @param DoliDB $db Datenbank-Handler
*/
public function __construct(DoliDB $db)
{
$this->db = $db;
}
/**
* Datensatz anlegen
*
* @param User $user Benutzer
* @param int $notrigger 1 = Trigger unterdrücken
* @return int >0 wenn OK, <=0 bei Fehler
*/
public function create(User $user, $notrigger = 0)
{
return $this->createCommon($user, $notrigger);
}
/**
* Datensatz laden
*
* @param int $id Rowid
* @param string $ref Referenz
* @return int >0 wenn OK, 0 wenn nicht gefunden, <0 bei Fehler
*/
public function fetch($id, $ref = null)
{
return $this->fetchCommon($id, $ref);
}
/**
* Alle Geräte eines Protokolls laden
*
* @param int $fk_protocol Protokoll-Rowid
* @return NetDiagDevice[] Liste der Geräte
*/
public function fetchAllByProtocol($fk_protocol)
{
$result = array();
$sql = "SELECT rowid FROM ".$this->db->prefix().$this->table_element;
$sql .= " WHERE fk_protocol = ".((int) $fk_protocol);
$sql .= " ORDER BY INET_ATON(ip), rowid";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$dev = new self($this->db);
if ($dev->fetch((int) $obj->rowid) > 0) {
$result[] = $dev;
}
}
}
return $result;
}
/**
* Datensatz aktualisieren
*
* @param User $user Benutzer
* @param int $notrigger 1 = Trigger unterdrücken
* @return int >0 wenn OK, <=0 bei Fehler
*/
public function update(User $user, $notrigger = 0)
{
return $this->updateCommon($user, $notrigger);
}
/**
* Datensatz löschen
*
* @param User $user Benutzer
* @param int $notrigger 1 = Trigger unterdrücken
* @return int >0 wenn OK, <=0 bei Fehler
*/
public function delete(User $user, $notrigger = 0)
{
return $this->deleteCommon($user, $notrigger);
}
}

View file

@ -0,0 +1,170 @@
<?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 htdocs/custom/netdiag/class/netdiagmeasurement.class.php
* \ingroup netdiag
* \brief Klasse für eine Messung (ein Tool-Lauf) im Diagnose-Protokoll
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
/**
* Klasse NetDiagMeasurement Ergebnis eines Tool-Laufs.
*
* params/result sind generisches JSON: neue Tools brauchen kein Schema-Update.
*/
class NetDiagMeasurement extends CommonObject
{
/** @var string Modul-Name */
public $module = 'netdiag';
/** @var string Element-Typ */
public $element = 'netdiagmeasurement';
/** @var string Datenbanktabelle (ohne Präfix) */
public $table_element = 'netdiag_measurement';
/** @var string Icon */
public $picto = 'fa-wave-square';
/** @var int 1 = Tabelle hat ein 'entity'-Feld */
public $ismultientitymanaged = 1;
/** Ampel-Status der Messung */
const RESULT_OK = 0;
const RESULT_WARN = 1;
const RESULT_FAIL = 2;
/**
* @var array<string,array<string,mixed>> Felddefinitionen
*/
public $fields = array(
'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'index' => 1, 'position' => 1),
'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'default' => 1, 'index' => 1, 'position' => 5),
'fk_protocol' => array('type' => 'integer', 'label' => 'Protocol', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'index' => 1, 'position' => 10),
'fk_device' => array('type' => 'integer', 'label' => 'Device', 'enabled' => 1, 'visible' => 0, 'index' => 1, 'position' => 15),
'tool' => array('type' => 'varchar(64)', 'label' => 'Tool', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'position' => 20),
'category' => array('type' => 'varchar(32)', 'label' => 'ToolCategory', 'enabled' => 1, 'visible' => 1, 'position' => 25),
'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'position' => 30),
'params' => array('type' => 'text', 'label' => 'Params', 'enabled' => 1, 'visible' => 0, 'position' => 40),
'result' => array('type' => 'text', 'label' => 'Result', 'enabled' => 1, 'visible' => 0, 'position' => 45),
'measure_status' => array('type' => 'smallint', 'label' => 'MeasureStatus', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'default' => 0, 'position' => 50),
'date_measure' => array('type' => 'datetime', 'label' => 'DateMeasure', 'enabled' => 1, 'visible' => 1, 'position' => 55),
'date_creation' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'position' => 500),
'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'position' => 501),
);
public $rowid;
public $entity;
public $fk_protocol;
public $fk_device;
public $tool;
public $category;
public $label;
public $params;
public $result;
public $measure_status;
public $date_measure;
public $date_creation;
public $tms;
/**
* Konstruktor
*
* @param DoliDB $db Datenbank-Handler
*/
public function __construct(DoliDB $db)
{
$this->db = $db;
}
/**
* Datensatz anlegen
*
* @param User $user Benutzer
* @param int $notrigger 1 = Trigger unterdrücken
* @return int >0 wenn OK, <=0 bei Fehler
*/
public function create(User $user, $notrigger = 0)
{
if (empty($this->date_measure)) {
$this->date_measure = dol_now();
}
return $this->createCommon($user, $notrigger);
}
/**
* Datensatz laden
*
* @param int $id Rowid
* @param string $ref Referenz
* @return int >0 wenn OK, 0 wenn nicht gefunden, <0 bei Fehler
*/
public function fetch($id, $ref = null)
{
return $this->fetchCommon($id, $ref);
}
/**
* Alle Messungen eines Protokolls laden
*
* @param int $fk_protocol Protokoll-Rowid
* @return NetDiagMeasurement[] Liste der Messungen
*/
public function fetchAllByProtocol($fk_protocol)
{
$result = array();
$sql = "SELECT rowid FROM ".$this->db->prefix().$this->table_element;
$sql .= " WHERE fk_protocol = ".((int) $fk_protocol);
$sql .= " ORDER BY date_measure, rowid";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$m = new self($this->db);
if ($m->fetch((int) $obj->rowid) > 0) {
$result[] = $m;
}
}
}
return $result;
}
/**
* Datensatz aktualisieren
*
* @param User $user Benutzer
* @param int $notrigger 1 = Trigger unterdrücken
* @return int >0 wenn OK, <=0 bei Fehler
*/
public function update(User $user, $notrigger = 0)
{
return $this->updateCommon($user, $notrigger);
}
/**
* Datensatz löschen
*
* @param User $user Benutzer
* @param int $notrigger 1 = Trigger unterdrücken
* @return int >0 wenn OK, <=0 bei Fehler
*/
public function delete(User $user, $notrigger = 0)
{
return $this->deleteCommon($user, $notrigger);
}
}

View file

@ -0,0 +1,233 @@
<?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 htdocs/custom/netdiag/class/netdiagprotocol.class.php
* \ingroup netdiag
* \brief Klasse für das Diagnose-Protokoll (Kopfdatensatz)
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
/**
* Klasse NetDiagProtocol ein Netzwerk-Diagnose-Vorgang beim Kunden.
*
* Bündelt gefundene Geräte (NetDiagDevice) und Messungen (NetDiagMeasurement)
* und ist mit Kunde (fk_soc) sowie optional Auftrag (fk_commande) verknüpft.
*/
class NetDiagProtocol extends CommonObject
{
/** @var string Modul-Name */
public $module = 'netdiag';
/** @var string Element-Typ */
public $element = 'netdiagprotocol';
/** @var string Datenbanktabelle (ohne Präfix) */
public $table_element = 'netdiag_protocol';
/** @var string Icon */
public $picto = 'fa-network-wired';
/** @var int 1 = Tabelle hat ein 'entity'-Feld für Multicompany */
public $ismultientitymanaged = 1;
/** Status-Konstanten */
const STATUS_DRAFT = 0;
const STATUS_DONE = 1;
/**
* @var array<string,array<string,mixed>> Felddefinitionen für die generischen CRUD-Methoden
*/
public $fields = array(
'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'index' => 1, 'position' => 1),
'ref' => array('type' => 'varchar(128)', 'label' => 'Ref', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 1, 'position' => 10),
'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'default' => 1, 'index' => 1, 'position' => 5),
'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'position' => 20),
'client_uuid' => array('type' => 'varchar(64)', 'label' => 'ClientUuid', 'enabled' => 1, 'visible' => 0, 'position' => 25),
'fk_soc' => array('type' => 'integer:Societe:societe/class/societe.class.php', 'label' => 'ThirdParty', 'enabled' => 1, 'visible' => 1, 'index' => 1, 'position' => 30),
'fk_commande' => array('type' => 'integer:Commande:commande/class/commande.class.php', 'label' => 'Order', 'enabled' => 1, 'visible' => 1, 'index' => 1, 'position' => 35),
'date_diag' => array('type' => 'datetime', 'label' => 'DateDiag', 'enabled' => 1, 'visible' => 1, 'position' => 40),
'fk_user_techniker' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'Technician', 'enabled' => 1, 'visible' => 1, 'position' => 45),
'standort' => array('type' => 'varchar(255)', 'label' => 'Location', 'enabled' => 1, 'visible' => 1, 'position' => 50),
'subnet' => array('type' => 'varchar(64)', 'label' => 'Subnet', 'enabled' => 1, 'visible' => 1, 'position' => 55),
'status' => array('type' => 'smallint', 'label' => 'Status', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'default' => 0, 'index' => 1, 'position' => 60, 'arrayofkeyval' => array(0 => 'Draft', 1 => 'Done')),
'note' => array('type' => 'text', 'label' => 'Note', 'enabled' => 1, 'visible' => 3, 'position' => 70),
'date_creation' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'position' => 500),
'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'position' => 501),
'fk_user_creat' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserAuthor', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'position' => 510),
'fk_user_modif' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserModif', 'enabled' => 1, 'visible' => 0, 'position' => 511),
);
public $rowid;
public $ref;
public $entity;
public $label;
public $client_uuid;
public $fk_soc;
public $fk_commande;
public $date_diag;
public $fk_user_techniker;
public $standort;
public $subnet;
public $status;
public $note;
public $date_creation;
public $tms;
public $fk_user_creat;
public $fk_user_modif;
/**
* Konstruktor
*
* @param DoliDB $db Datenbank-Handler
*/
public function __construct(DoliDB $db)
{
$this->db = $db;
}
/**
* Datensatz anlegen
*
* @param User $user Benutzer
* @param int $notrigger 1 = Trigger unterdrücken
* @return int >0 wenn OK (rowid), <=0 bei Fehler
*/
public function create(User $user, $notrigger = 0)
{
if (empty($this->ref) || $this->ref == '(PROV)') {
$this->ref = $this->getNextNumRef();
}
if (empty($this->date_diag)) {
$this->date_diag = dol_now();
}
return $this->createCommon($user, $notrigger);
}
/**
* Datensatz laden
*
* @param int $id Rowid
* @param string $ref Referenz statt Rowid
* @return int >0 wenn OK, 0 wenn nicht gefunden, <0 bei Fehler
*/
public function fetch($id, $ref = null)
{
return $this->fetchCommon($id, $ref);
}
/**
* Protokoll anhand der Client-UUID laden (für idempotenten App-Sync)
*
* @param string $uuid Client-UUID
* @return int >0 wenn OK, 0 wenn nicht gefunden, <0 bei Fehler
*/
public function fetchByClientUuid($uuid)
{
$sql = "SELECT rowid FROM ".$this->db->prefix().$this->table_element;
$sql .= " WHERE client_uuid = '".$this->db->escape($uuid)."'";
$sql .= " AND entity IN (".getEntity($this->element).")";
$resql = $this->db->query($sql);
if (!$resql) {
$this->error = $this->db->lasterror();
return -1;
}
if ($obj = $this->db->fetch_object($resql)) {
return $this->fetch((int) $obj->rowid);
}
return 0;
}
/**
* Datensatz aktualisieren
*
* @param User $user Benutzer
* @param int $notrigger 1 = Trigger unterdrücken
* @return int >0 wenn OK, <=0 bei Fehler
*/
public function update(User $user, $notrigger = 0)
{
return $this->updateCommon($user, $notrigger);
}
/**
* Datensatz löschen (inkl. zugehöriger Geräte und Messungen)
*
* @param User $user Benutzer
* @param int $notrigger 1 = Trigger unterdrücken
* @return int >0 wenn OK, <=0 bei Fehler
*/
public function delete(User $user, $notrigger = 0)
{
$this->db->begin();
$tables = array('netdiag_measurement', 'netdiag_device');
foreach ($tables as $table) {
$sql = "DELETE FROM ".$this->db->prefix().$table." WHERE fk_protocol = ".((int) $this->id);
if (!$this->db->query($sql)) {
$this->error = $this->db->lasterror();
$this->db->rollback();
return -1;
}
}
$result = $this->deleteCommon($user, $notrigger);
if ($result <= 0) {
$this->db->rollback();
return -1;
}
$this->db->commit();
return 1;
}
/**
* Nächste freie Referenz ermitteln (NDjjjj-nnnn)
*
* @return string Neue Referenz
*/
public function getNextNumRef()
{
$prefix = 'ND'.dol_print_date(dol_now(), '%Y').'-';
$sql = "SELECT MAX(CAST(SUBSTRING(ref, ".(strlen($prefix) + 1).") AS UNSIGNED)) AS maxnum";
$sql .= " FROM ".$this->db->prefix().$this->table_element;
$sql .= " WHERE ref LIKE '".$this->db->escape($prefix)."%'";
$sql .= " AND entity IN (".getEntity($this->element).")";
$resql = $this->db->query($sql);
$num = 0;
if ($resql && ($obj = $this->db->fetch_object($resql))) {
$num = (int) $obj->maxnum;
}
return $prefix.sprintf('%04d', $num + 1);
}
/**
* Beschriftung des Status zurückgeben
*
* @param int $mode 0=Langtext, 1=Kurztext, 3=Picto, ...
* @return string HTML-Label
*/
public function getLibStatut($mode = 0)
{
global $langs;
$labels = array(
self::STATUS_DRAFT => $langs->trans('Draft'),
self::STATUS_DONE => $langs->trans('NetDiagStatusDone'),
);
$label = isset($labels[$this->status]) ? $labels[$this->status] : '';
$statustype = ($this->status == self::STATUS_DONE) ? 'status4' : 'status0';
return dolGetStatus($label, $label, '', $statustype, $mode);
}
}

View file

@ -0,0 +1,240 @@
<?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/>.
*/
/**
* \defgroup netdiag Modul NetDiag
* \brief NetDiag Modul-Descriptor Netzwerk-Diagnose-Protokolle.
* \file htdocs/custom/netdiag/core/modules/modNetDiag.class.php
* \ingroup netdiag
* \brief Beschreibungs- und Aktivierungsdatei für das Modul NetDiag
*/
include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php';
/**
* Beschreibungs- und Aktivierungsklasse für das Modul NetDiag
*
* NetDiag speichert Netzwerk-Diagnose-Protokolle (IP-Scan, Port-Scan, Ping,
* WLAN, DHCP, SNMP, Durchsatz, Stresstest) und hängt sie an Kunde und Auftrag.
* Die JSON-API unter /custom/netdiag/api/ versorgt die mobile Diagnose-App.
*/
class modNetDiag extends DolibarrModules
{
/**
* Konstruktor. Definiert Namen, Konstanten, Verzeichnisse, Rechte, Menüs.
*
* @param DoliDB $db Datenbank-Handler
*/
public function __construct($db)
{
global $conf, $langs;
$this->db = $db;
// Eindeutige Modul-ID (freier Bereich, siehe Wiki Liste der Modul-IDs)
$this->numero = 500100;
// Schlüsseltext zur Identifikation (Rechte, Menüs, ...)
$this->rights_class = 'netdiag';
// Familie: 'technic' = transversales/technisches Modul
$this->family = "technic";
$this->module_position = '90';
// Modul-Label (ohne Leerzeichen), Fallback wenn Übersetzung 'ModuleNetDiagName' fehlt
$this->name = preg_replace('/^mod/i', '', get_class($this));
// Modulbeschreibung, Fallback wenn Übersetzung 'ModuleNetDiagDesc' fehlt
$this->description = "ModuleNetDiagDesc";
$this->descriptionlong = "Netzwerk-Diagnose-Protokolle: erfasst per mobiler App Geräte, Ports, Messungen und hängt sie an Kunde und Auftrag.";
// Autor
$this->editor_name = 'Alles Watt läuft';
$this->editor_url = '';
$this->editor_squarred_logo = '';
$this->version = '1.0.0';
// Konstanten-Name in llx_const für Modul-Status
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
// Icon (Font-Awesome)
$this->picto = 'fa-network-wired';
// Vom Modul unterstützte Funktionen
$this->module_parts = array(
'triggers' => 0,
'login' => 0,
'substitutions' => 0,
'menus' => 0,
'tpl' => 0,
'barcode' => 0,
'models' => 1,
'printing' => 0,
'theme' => 0,
'css' => array(),
'js' => array(),
'hooks' => array(),
'moduleforexternal' => 0,
'websitetemplates' => 0,
'captcha' => 0,
);
// Datenverzeichnisse, die beim Aktivieren angelegt werden
$this->dirs = array("/netdiag/temp");
// Konfigurationsseiten (in netdiag/admin)
$this->config_page_url = array("setup.php@netdiag");
// Abhängigkeiten
$this->hidden = false;
$this->depends = array();
$this->requiredby = array();
$this->conflictwith = array();
// Sprachdatei des Moduls
$this->langfiles = array("netdiag@netdiag");
// Voraussetzungen
$this->phpmin = array(7, 4);
$this->need_dolibarr_version = array(19, -3);
$this->need_javascript_ajax = 0;
$this->warnings_activation = array();
$this->warnings_activation_ext = array();
// Konstanten beim Aktivieren anlegen
// (Key, Typ, Wert, Beschreibung, sichtbar, current/allentities, deleteonunactive)
$this->const = array(
1 => array('NETDIAG_API_JWT_SECRET', 'chaine', dol_hash(dol_print_date(dol_now(), 'dayhourrfc').mt_rand(), 'md5'), 'Geheimer Schlüssel zum Signieren der API-JWT', 0, 'current', 1),
2 => array('NETDIAG_API_TOKEN_TTL', 'chaine', '604800', 'Gültigkeit des API-Tokens in Sekunden (Standard 7 Tage)', 1, 'current', 0),
3 => array('NETDIAG_APK_URL', 'chaine', 'https://git.data-it-solution.de/api/packages/data-it/generic/netdiag-apk/latest/NetDiag.apk', 'Download-URL der Android-App (für QR-Code im Admin)', 1, 'current', 0),
);
if (!isModEnabled("netdiag")) {
$conf->netdiag = new stdClass();
$conf->netdiag->enabled = 0;
}
// Neue Tabs an bestehenden Objekten
// Tab "Netzwerk-Diagnose" an Kunde (thirdparty) und Auftrag (order)
$this->tabs = array(
'thirdparty:+netdiag:NetDiagTab:netdiag@netdiag:$user->hasRight(\'netdiag\', \'protocol\', \'read\'):/custom/netdiag/netdiag_object_tab.php?socid=__ID__&objecttype=thirdparty',
'order:+netdiag:NetDiagTab:netdiag@netdiag:$user->hasRight(\'netdiag\', \'protocol\', \'read\'):/custom/netdiag/netdiag_object_tab.php?id=__ID__&objecttype=order',
);
$this->dictionaries = array();
$this->boxes = array();
$this->cronjobs = array();
// Rechte
$this->rights = array();
$r = 0;
$o = 1;
$this->rights[$r][0] = $this->numero.sprintf("%02d", ($o * 10) + 1);
$this->rights[$r][1] = 'Diagnose-Protokolle lesen';
$this->rights[$r][4] = 'protocol';
$this->rights[$r][5] = 'read';
$r++;
$this->rights[$r][0] = $this->numero.sprintf("%02d", ($o * 10) + 2);
$this->rights[$r][1] = 'Diagnose-Protokolle anlegen/ändern';
$this->rights[$r][4] = 'protocol';
$this->rights[$r][5] = 'write';
$r++;
$this->rights[$r][0] = $this->numero.sprintf("%02d", ($o * 10) + 3);
$this->rights[$r][1] = 'Diagnose-Protokolle löschen';
$this->rights[$r][4] = 'protocol';
$this->rights[$r][5] = 'delete';
$r++;
// Top-Menü
$this->menu = array();
$r = 0;
$this->menu[$r++] = array(
'fk_menu' => '',
'type' => 'top',
'titre' => 'ModuleNetDiagName',
'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle"'),
'mainmenu' => 'netdiag',
'leftmenu' => '',
'url' => '/custom/netdiag/netdiagindex.php',
'langs' => 'netdiag@netdiag',
'position' => 1000 + $r,
'enabled' => 'isModEnabled("netdiag")',
'perms' => '$user->hasRight("netdiag", "protocol", "read")',
'target' => '',
'user' => 2,
);
$this->menu[$r++] = array(
'fk_menu' => 'fk_mainmenu=netdiag',
'type' => 'left',
'titre' => 'NetDiagProtocolList',
'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle paddingright"'),
'mainmenu' => 'netdiag',
'leftmenu' => 'netdiag_protocol_list',
'url' => '/custom/netdiag/netdiagindex.php',
'langs' => 'netdiag@netdiag',
'position' => 1000 + $r,
'enabled' => 'isModEnabled("netdiag")',
'perms' => '$user->hasRight("netdiag", "protocol", "read")',
'target' => '',
'user' => 2,
);
}
/**
* Wird beim Aktivieren des Moduls aufgerufen.
* Legt Konstanten, Rechte, Menüs und Datenbanktabellen an.
*
* @param string $options Optionen ('', 'noboxes')
* @return int<-1,1> 1 wenn OK, <=0 bei Fehler
*/
public function init($options = '')
{
global $conf, $langs;
// SQL-Tabellen beim Aktivieren laden
$result = $this->_load_tables('/netdiag/sql/');
if ($result < 0) {
return -1;
}
// Rechte entfernen/neu setzen
$this->remove($options);
$sql = array();
return $this->_init($sql, $options);
}
/**
* Wird beim Deaktivieren des Moduls aufgerufen.
*
* @param string $options Optionen ('', 'noboxes')
* @return int<-1,1> 1 wenn OK, <=0 bei Fehler
*/
public function remove($options = '')
{
$sql = array();
return $this->_remove($sql, $options);
}
}

61
langs/de_DE/netdiag.lang Normal file
View file

@ -0,0 +1,61 @@
# Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
# Sprachdatei NetDiag - Deutsch
#
# Allgemein
#
ModuleNetDiagName = NetDiag
ModuleNetDiagDesc = Netzwerk-Diagnose: erfasst per mobiler App Geräte, Ports und Messungen und hängt die Protokolle an Kunde und Auftrag.
NetDiagArea = NetDiag - Übersicht
NetDiagTab = Netzwerk-Diagnose
#
# Admin-Seite
#
NetDiagSetup = NetDiag Einrichtung
NetDiagSetupPage = NetDiag Einrichtungsseite
NetDiagAbout = Über NetDiag
NetDiagAboutPage = NetDiag Info-Seite
NETDIAG_API_TOKEN_TTL = Token-Gültigkeit (Sekunden)
NETDIAG_API_TOKEN_TTLTooltip = Wie lange ein API-Token der mobilen App gültig bleibt. Standard 604800 (7 Tage).
NETDIAG_APK_URL = Download-URL der App (APK)
NETDIAG_APK_URLTooltip = Adresse, unter der die Android-App (APK) liegt. Wird als QR-Code angezeigt.
NetDiagApiSecretInfo = Der JWT-Schlüssel wurde beim Aktivieren automatisch erzeugt und ist nicht editierbar.
NetDiagAppDownload = NetDiag-App herunterladen
NetDiagAppDownloadHint = Mit dem Handy scannen, um die Diagnose-App zu installieren.
#
# Protokoll
#
NetDiagProtocol = Diagnose-Protokoll
NetDiagProtocols = Diagnose-Protokolle
NetDiagProtocolList = Protokoll-Liste
NewNetDiagProtocol = Neues Diagnose-Protokoll
NetDiagStatusDraft = Entwurf
NetDiagStatusDone = Abgeschlossen
NetDiagNoProtocol = Noch kein Diagnose-Protokoll für dieses Objekt vorhanden
#
# Felder
#
DateDiag = Aufnahmedatum
DateMeasure = Messzeitpunkt
Technician = Techniker
Location = Standort
Subnet = Netzbereich
DeviceType = Gerätetyp
Protocol = Protokoll
Device = Gerät
Tool = Werkzeug
ToolCategory = Kategorie
MeasureStatus = Bewertung
ClientUuid = App-Kennung
IpAddress = IP-Adresse
MacAddress = MAC-Adresse
Hostname = Hostname
Vendor = Hersteller
NetDiagDevices = Gefundene Geräte
NetDiagMeasurements = Messungen
NetDiagMeasureOk = OK
NetDiagMeasureWarn = Warnung
NetDiagMeasureFail = Fehler

61
langs/en_US/netdiag.lang Normal file
View file

@ -0,0 +1,61 @@
# Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
# Translation file NetDiag - English
#
# Generic
#
ModuleNetDiagName = NetDiag
ModuleNetDiagDesc = Network diagnostics: capture devices, ports and measurements with a mobile app and attach the protocols to customer and order.
NetDiagArea = NetDiag - Overview
NetDiagTab = Network diagnostics
#
# Admin page
#
NetDiagSetup = NetDiag setup
NetDiagSetupPage = NetDiag setup page
NetDiagAbout = About NetDiag
NetDiagAboutPage = NetDiag about page
NETDIAG_API_TOKEN_TTL = Token validity (seconds)
NETDIAG_API_TOKEN_TTLTooltip = How long a mobile app API token stays valid. Default 604800 (7 days).
NETDIAG_APK_URL = App download URL (APK)
NETDIAG_APK_URLTooltip = Address where the Android app (APK) is hosted. Shown as a QR code.
NetDiagApiSecretInfo = The JWT secret was generated automatically on activation and is not editable.
NetDiagAppDownload = Download NetDiag app
NetDiagAppDownloadHint = Scan with your phone to install the diagnostics app.
#
# Protocol
#
NetDiagProtocol = Diagnostic protocol
NetDiagProtocols = Diagnostic protocols
NetDiagProtocolList = Protocol list
NewNetDiagProtocol = New diagnostic protocol
NetDiagStatusDraft = Draft
NetDiagStatusDone = Completed
NetDiagNoProtocol = No diagnostic protocol for this object yet
#
# Fields
#
DateDiag = Diagnostic date
DateMeasure = Measurement time
Technician = Technician
Location = Location
Subnet = Subnet
DeviceType = Device type
Protocol = Protocol
Device = Device
Tool = Tool
ToolCategory = Category
MeasureStatus = Rating
ClientUuid = App identifier
IpAddress = IP address
MacAddress = MAC address
Hostname = Hostname
Vendor = Vendor
NetDiagDevices = Discovered devices
NetDiagMeasurements = Measurements
NetDiagMeasureOk = OK
NetDiagMeasureWarn = Warning
NetDiagMeasureFail = Fail

134
lib/netdiag.lib.php Normal file
View file

@ -0,0 +1,134 @@
<?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 htdocs/custom/netdiag/lib/netdiag.lib.php
* \ingroup netdiag
* \brief Hilfsfunktionen für das Modul NetDiag
*/
/**
* Tabs für die Admin-Seiten des Moduls vorbereiten
*
* @return array<int,array<int,string>> Tab-Array
*/
function netdiagAdminPrepareHead()
{
global $langs, $conf;
$langs->load("netdiag@netdiag");
$h = 0;
$head = array();
$head[$h][0] = dol_buildpath("/netdiag/admin/setup.php", 1);
$head[$h][1] = $langs->trans("Settings");
$head[$h][2] = 'settings';
$h++;
$head[$h][0] = dol_buildpath("/netdiag/admin/about.php", 1);
$head[$h][1] = $langs->trans("About");
$head[$h][2] = 'about';
$h++;
complete_head_from_modules($conf, $langs, null, $head, $h, 'netdiag@netdiag');
complete_head_from_modules($conf, $langs, null, $head, $h, 'netdiag@netdiag', 'remove');
return $head;
}
/**
* Tabs für die Detailansicht eines Diagnose-Protokolls vorbereiten
*
* @param NetDiagProtocol $object Protokoll-Objekt
* @return array<int,array<int,string>> Tab-Array
*/
function netdiagProtocolPrepareHead($object)
{
global $langs, $conf;
$langs->load("netdiag@netdiag");
$h = 0;
$head = array();
$head[$h][0] = dol_buildpath("/netdiag/netdiagprotocol_card.php", 1).'?id='.$object->id;
$head[$h][1] = $langs->trans("NetDiagProtocol");
$head[$h][2] = 'card';
$h++;
complete_head_from_modules($conf, $langs, $object, $head, $h, 'netdiagprotocol@netdiag');
complete_head_from_modules($conf, $langs, $object, $head, $h, 'netdiagprotocol@netdiag', 'remove');
return $head;
}
/**
* Ausgabeverzeichnis des Moduls ermitteln (für PDF-Dokumente)
*
* @return string Absoluter Pfad zum Dokumentenverzeichnis
*/
function netdiagGetOutputDir()
{
global $conf;
if (!empty($conf->netdiag->dir_output)) {
return $conf->netdiag->dir_output;
}
return DOL_DATA_ROOT.'/netdiag';
}
/**
* Ein Mess-Ergebnis (JSON) lesbar als HTML aufbereiten.
*
* Generisch: rendert flache Schlüssel/Wert-Paare und einfache Listen,
* damit auch künftige Tools ohne Code-Änderung dargestellt werden.
*
* @param string $json JSON-String des Ergebnisses
* @return string HTML-Schnipsel
*/
function netdiagFormatResult($json)
{
if (empty($json)) {
return '<span class="opacitymedium">-</span>';
}
$data = json_decode($json, true);
if ($data === null) {
return dol_escape_htmltag(dol_trunc($json, 120));
}
if (!is_array($data)) {
return dol_escape_htmltag((string) $data);
}
$out = '<div class="netdiag-result">';
foreach ($data as $key => $val) {
$label = dol_escape_htmltag(ucfirst((string) $key));
if (is_array($val)) {
$flat = array();
foreach ($val as $item) {
$flat[] = is_array($item) ? json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $item;
}
$valstr = implode(', ', $flat);
} elseif (is_bool($val)) {
$valstr = $val ? 'ja' : 'nein';
} else {
$valstr = (string) $val;
}
$out .= '<span class="netdiag-kv"><strong>'.$label.':</strong> '.dol_escape_htmltag(dol_trunc($valstr, 200)).'</span> ';
}
$out .= '</div>';
return $out;
}

199
lib/netdiag_pdf.lib.php Normal file
View file

@ -0,0 +1,199 @@
<?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');
$pdf->SetFillColor(0, 70, 130);
$pdf->SetTextColor(255, 255, 255);
$pdf->SetFont('', 'B', 8);
$cols = array(35, 40, 50, 40, 15);
$heads = array("IpAddress", "MacAddress", "Hostname", "Vendor", "DeviceType");
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);
foreach ($devices as $dev) {
$pdf->Cell($cols[0], 5, dol_trunc($dev->ip, 22), 1, 0, 'L');
$pdf->Cell($cols[1], 5, dol_trunc($dev->mac, 24), 1, 0, 'L');
$pdf->Cell($cols[2], 5, dol_trunc($dev->hostname, 32), 1, 0, 'L');
$pdf->Cell($cols[3], 5, dol_trunc($dev->vendor, 26), 1, 0, 'L');
$pdf->Cell($cols[4], 5, dol_trunc($dev->devicetype, 10), 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');
$statuslabels = array(0 => 'OK', 1 => $outputlangs->transnoentities("NetDiagMeasureWarn"), 2 => $outputlangs->transnoentities("NetDiagMeasureFail"));
foreach ($measurements as $m) {
$st = (int) $m->measure_status;
if ($st == 2) {
$pdf->SetFillColor(230, 130, 130);
} elseif ($st == 1) {
$pdf->SetFillColor(245, 215, 130);
} else {
$pdf->SetFillColor(170, 215, 170);
}
$pdf->SetFont('', 'B', 9);
$title = ($m->category ? '['.$m->category.'] ' : '').$m->tool.($m->label ? ' — '.$m->label : '');
$pdf->Cell(150, 6, dol_trunc($title, 80), 1, 0, 'L');
$pdf->Cell(30, 6, $statuslabels[$st], 1, 1, 'C', true);
$pdf->SetFont('', '', 8);
$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 (string) $json;
}
$parts = array();
foreach ($data as $key => $val) {
if (is_array($val)) {
$flat = array();
foreach ($val as $item) {
$flat[] = is_array($item) ? json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $item;
}
$val = implode(', ', $flat);
} elseif (is_bool($val)) {
$val = $val ? 'ja' : 'nein';
}
$parts[] = ucfirst((string) $key).': '.$val;
}
return implode(' | ', $parts);
}

170
netdiag_object_tab.php Normal file
View file

@ -0,0 +1,170 @@
<?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/netdiag_object_tab.php
* \ingroup netdiag
* \brief Tab "Netzwerk-Diagnose" an Kunde (thirdparty) und Auftrag (order)
*/
// 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.'/core/lib/company.lib.php';
require_once __DIR__.'/class/netdiagprotocol.class.php';
/**
* @var DoliDB $db
* @var Translate $langs
* @var User $user
*/
$langs->loadLangs(array("netdiag@netdiag", "companies", "orders"));
if (!$user->hasRight('netdiag', 'protocol', 'read')) {
accessforbidden();
}
$objecttype = GETPOST('objecttype', 'aZ09');
$id = GETPOSTINT('id');
$socid = GETPOSTINT('socid');
// Parent-Objekt laden und passenden Filter aufbauen
$parent = null;
$head = array();
$picto = 'company';
$filtersql = '';
if ($objecttype == 'order') {
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/order.lib.php';
$parent = new Commande($db);
if ($id <= 0 || $parent->fetch($id) <= 0) {
accessforbidden('Order not found');
}
$parent->fetch_thirdparty();
$head = commande_prepare_head($parent);
$picto = 'order';
$filtersql = " AND p.fk_commande = ".((int) $parent->id);
} else {
$parent = new Societe($db);
if ($socid <= 0 || $parent->fetch($socid) <= 0) {
accessforbidden('ThirdParty not found');
}
$head = societe_prepare_head($parent);
$picto = 'company';
$filtersql = " AND p.fk_soc = ".((int) $parent->id);
}
/*
* Ansicht
*/
llxHeader('', $langs->trans("NetDiagTab"), '', '', 0, 0, '', '', '', 'mod-netdiag');
print dol_get_fiche_head($head, 'netdiag', $langs->trans($objecttype == 'order' ? 'CustomerOrder' : 'ThirdParty'), -1, $picto);
if ($objecttype == 'order') {
$linkback = '<a href="'.DOL_URL_ROOT.'/commande/list.php?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>';
dol_banner_tab($parent, 'ref', $linkback, 1, 'ref', 'ref');
} else {
$linkback = '<a href="'.DOL_URL_ROOT.'/societe/list.php?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>';
dol_banner_tab($parent, 'socid', $linkback, ($user->socid ? 0 : 1), 'rowid', 'nom');
}
print dol_get_fiche_end();
// Protokolle des Objekts auflisten
$sql = "SELECT p.rowid, p.ref, p.label, p.date_diag, p.standort, p.subnet, p.status,";
$sql .= " (SELECT COUNT(*) FROM ".$db->prefix()."netdiag_device d WHERE d.fk_protocol = p.rowid) as devcount,";
$sql .= " (SELECT COUNT(*) FROM ".$db->prefix()."netdiag_measurement m WHERE m.fk_protocol = p.rowid) as meascount";
$sql .= " FROM ".$db->prefix()."netdiag_protocol as p";
$sql .= " WHERE p.entity IN (".getEntity('netdiagprotocol').")";
$sql .= $filtersql;
$sql .= " ORDER BY p.date_diag DESC, p.rowid DESC";
$resql = $db->query($sql);
print '<div class="fichecenter"><br>';
print load_fiche_titre($langs->trans("NetDiagProtocols"), '', 'fa-network-wired');
print '<div class="div-table-responsive-no-min">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans("Ref").'</th>';
print '<th>'.$langs->trans("DateDiag").'</th>';
print '<th>'.$langs->trans("Location").'</th>';
print '<th>'.$langs->trans("Subnet").'</th>';
print '<th class="right">'.$langs->trans("NetDiagDevices").'</th>';
print '<th class="right">'.$langs->trans("NetDiagMeasurements").'</th>';
print '<th class="center">'.$langs->trans("Status").'</th>';
print '</tr>';
$proto = new NetDiagProtocol($db);
$nb = 0;
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$nb++;
$proto->status = $obj->status;
$cardurl = dol_buildpath('/netdiag/netdiagprotocol_card.php', 1).'?id='.$obj->rowid;
print '<tr class="oddeven">';
print '<td><a href="'.$cardurl.'">'.img_picto('', 'fa-network-wired').' '.dol_escape_htmltag($obj->ref).'</a></td>';
print '<td>'.dol_print_date($db->jdate($obj->date_diag), 'dayhour').'</td>';
print '<td>'.dol_escape_htmltag($obj->standort).'</td>';
print '<td>'.dol_escape_htmltag($obj->subnet).'</td>';
print '<td class="right">'.((int) $obj->devcount).'</td>';
print '<td class="right">'.((int) $obj->meascount).'</td>';
print '<td class="center">'.$proto->getLibStatut(3).'</td>';
print '</tr>';
}
}
if (!$nb) {
print '<tr><td colspan="7" class="opacitymedium center">'.$langs->trans("NetDiagNoProtocol").'</td></tr>';
}
print '</table>';
print '</div>';
print '</div>';
llxFooter();
$db->close();

158
netdiagindex.php Normal file
View file

@ -0,0 +1,158 @@
<?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/netdiagindex.php
* \ingroup netdiag
* \brief Liste aller Netzwerk-Diagnose-Protokolle (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.'/core/class/html.formcompany.class.php';
require_once __DIR__.'/class/netdiagprotocol.class.php';
/**
* @var DoliDB $db
* @var Translate $langs
* @var User $user
*/
$langs->loadLangs(array("netdiag@netdiag", "companies", "orders"));
if (!$user->hasRight('netdiag', 'protocol', 'read')) {
accessforbidden();
}
$search_ref = GETPOST('search_ref', 'alpha');
$search_soc = GETPOST('search_soc', 'alpha');
$limit = GETPOSTINT('limit') ? GETPOSTINT('limit') : $conf->liste_limit;
$page = GETPOSTINT('page');
if ($page < 0) {
$page = 0;
}
$offset = $limit * $page;
/*
* Ansicht
*/
$form = new Form($db);
$title = $langs->trans("NetDiagProtocols");
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-netdiag page-list');
// Datensätze laden
$sql = "SELECT p.rowid, p.ref, p.label, p.date_diag, p.standort, p.subnet, p.status,";
$sql .= " s.rowid as socid, s.nom as socname,";
$sql .= " (SELECT COUNT(*) FROM ".$db->prefix()."netdiag_device d WHERE d.fk_protocol = p.rowid) as devcount";
$sql .= " FROM ".$db->prefix()."netdiag_protocol as p";
$sql .= " LEFT JOIN ".$db->prefix()."societe as s ON s.rowid = p.fk_soc";
$sql .= " WHERE p.entity IN (".getEntity('netdiagprotocol').")";
if ($search_ref) {
$sql .= natural_search('p.ref', $search_ref);
}
if ($search_soc) {
$sql .= natural_search('s.nom', $search_soc);
}
$sql .= " ORDER BY p.date_diag DESC, p.rowid DESC";
$sql .= $db->plimit($limit, $offset);
$resql = $db->query($sql);
$rows = array();
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$rows[] = $obj;
}
}
print load_fiche_titre($title, '', 'fa-network-wired');
print '<form method="GET" action="'.$_SERVER["PHP_SELF"].'">';
print '<div class="div-table-responsive">';
print '<table class="tagtable liste">';
print '<tr class="liste_titre">';
print '<td><input type="text" name="search_ref" class="maxwidth100" value="'.dol_escape_htmltag($search_ref).'" placeholder="'.$langs->trans("Ref").'"></td>';
print '<td><input type="text" name="search_soc" class="maxwidth150" value="'.dol_escape_htmltag($search_soc).'" placeholder="'.$langs->trans("ThirdParty").'"></td>';
print '<td colspan="4" class="right"><input type="submit" class="button small" value="'.$langs->trans("Search").'"></td>';
print '</tr>';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans("Ref").'</th>';
print '<th>'.$langs->trans("ThirdParty").'</th>';
print '<th>'.$langs->trans("DateDiag").'</th>';
print '<th>'.$langs->trans("Location").'</th>';
print '<th class="right">'.$langs->trans("NetDiagDevices").'</th>';
print '<th class="center">'.$langs->trans("Status").'</th>';
print '</tr>';
$proto = new NetDiagProtocol($db);
foreach ($rows as $obj) {
$proto->id = $obj->rowid;
$proto->status = $obj->status;
$cardurl = dol_buildpath('/netdiag/netdiagprotocol_card.php', 1).'?id='.$obj->rowid;
print '<tr class="oddeven">';
print '<td><a href="'.$cardurl.'">'.img_picto('', 'fa-network-wired').' '.dol_escape_htmltag($obj->ref).'</a></td>';
if ($obj->socid) {
print '<td><a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.$obj->socid.'">'.dol_escape_htmltag($obj->socname).'</a></td>';
} else {
print '<td></td>';
}
print '<td>'.dol_print_date($db->jdate($obj->date_diag), 'dayhour').'</td>';
print '<td>'.dol_escape_htmltag($obj->standort).'</td>';
print '<td class="right">'.((int) $obj->devcount).'</td>';
print '<td class="center">'.$proto->getLibStatut(3).'</td>';
print '</tr>';
}
if (empty($rows)) {
print '<tr><td colspan="6" class="opacitymedium center">'.$langs->trans("NoRecordFound").'</td></tr>';
}
print '</table>';
print '</div>';
print '</form>';
llxFooter();
$db->close();

231
netdiagprotocol_card.php Normal file
View file

@ -0,0 +1,231 @@
<?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("Vendor").'</th><th>'.$langs->trans("DeviceType").'</th>';
print '</tr>';
foreach ($devices as $dev) {
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($dev->hostname).'</td>';
print '<td>'.dol_escape_htmltag($dev->vendor).'</td>';
print '<td>'.dol_escape_htmltag($dev->devicetype).'</td>';
print '</tr>';
}
if (empty($devices)) {
print '<tr><td colspan="5" 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');
$statuscss = array(0 => 'badge-status4', 1 => 'badge-status1', 2 => 'badge-status8');
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;
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).'</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();

View file

@ -0,0 +1,6 @@
-- Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
--
-- Schlüssel und Indizes für llx_netdiag_device
ALTER TABLE llx_netdiag_device ADD INDEX idx_netdiag_device_fk_protocol(fk_protocol);
ALTER TABLE llx_netdiag_device ADD CONSTRAINT fk_netdiag_device_protocol FOREIGN KEY (fk_protocol) REFERENCES llx_netdiag_protocol(rowid);

View file

@ -0,0 +1,22 @@
-- 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.
--
-- Tabelle: gefundenes Gerät innerhalb eines Diagnose-Protokolls
CREATE TABLE llx_netdiag_device(
rowid integer AUTO_INCREMENT PRIMARY KEY NOT NULL,
entity integer DEFAULT 1 NOT NULL,
fk_protocol integer NOT NULL,
ip varchar(45),
mac varchar(17),
hostname varchar(255),
vendor varchar(128),
devicetype varchar(64),
note text,
date_creation datetime NOT NULL,
tms timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
) ENGINE=innodb;

View file

@ -0,0 +1,8 @@
-- Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
--
-- Schlüssel und Indizes für llx_netdiag_measurement
ALTER TABLE llx_netdiag_measurement ADD INDEX idx_netdiag_measurement_fk_protocol(fk_protocol);
ALTER TABLE llx_netdiag_measurement ADD INDEX idx_netdiag_measurement_fk_device(fk_device);
ALTER TABLE llx_netdiag_measurement ADD INDEX idx_netdiag_measurement_tool(tool);
ALTER TABLE llx_netdiag_measurement ADD CONSTRAINT fk_netdiag_measurement_protocol FOREIGN KEY (fk_protocol) REFERENCES llx_netdiag_protocol(rowid);

View file

@ -0,0 +1,25 @@
-- 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.
--
-- Tabelle: ein Tool-Lauf (Messung) innerhalb eines Diagnose-Protokolls.
-- params/result sind generisches JSON -> neue Tools brauchen kein Schema-Update.
CREATE TABLE llx_netdiag_measurement(
rowid integer AUTO_INCREMENT PRIMARY KEY NOT NULL,
entity integer DEFAULT 1 NOT NULL,
fk_protocol integer NOT NULL,
fk_device integer,
tool varchar(64) NOT NULL,
category varchar(32),
label varchar(255),
params text,
result longtext,
measure_status smallint DEFAULT 0 NOT NULL,
date_measure datetime,
date_creation datetime NOT NULL,
tms timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
) ENGINE=innodb;

View file

@ -0,0 +1,8 @@
-- Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
--
-- Schlüssel und Indizes für llx_netdiag_protocol
ALTER TABLE llx_netdiag_protocol ADD UNIQUE INDEX uk_netdiag_protocol_ref(ref, entity);
ALTER TABLE llx_netdiag_protocol ADD UNIQUE INDEX uk_netdiag_protocol_uuid(client_uuid);
ALTER TABLE llx_netdiag_protocol ADD INDEX idx_netdiag_protocol_fk_soc(fk_soc);
ALTER TABLE llx_netdiag_protocol ADD INDEX idx_netdiag_protocol_fk_commande(fk_commande);

View file

@ -0,0 +1,28 @@
-- 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.
--
-- Tabelle: Diagnose-Protokoll (Kopfdatensatz eines Netzwerk-Einsatzes)
CREATE TABLE llx_netdiag_protocol(
rowid integer AUTO_INCREMENT PRIMARY KEY NOT NULL,
ref varchar(128) NOT NULL,
entity integer DEFAULT 1 NOT NULL,
label varchar(255),
client_uuid varchar(64),
fk_soc integer,
fk_commande integer,
date_diag datetime,
fk_user_techniker integer,
standort varchar(255),
subnet varchar(64),
status smallint DEFAULT 0 NOT NULL,
note text,
date_creation datetime NOT NULL,
tms timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
fk_user_creat integer NOT NULL,
fk_user_modif integer
) ENGINE=innodb;