/** * API-Client für die NetDiag-Dolibarr-Schnittstelle. * * Spricht das Modul `netdiag` unter /custom/netdiag/api/ an. Authentifizierung * per JWT (Bearer). Im Browser-Dev läuft alles über den Vite-Proxy, auf dem * Gerät über die in den Einstellungen hinterlegte Server-URL. */ import { Preferences } from '@capacitor/preferences'; import type { Customer, Order, Protocol } from './types'; const API_PATH = '/custom/netdiag/api'; const FETCH_TIMEOUT_MS = 15_000; let serverUrl = ''; let token = ''; /** Callback, der bei 401 (Sitzung abgelaufen) ausgelöst wird */ let onAuthFailure: (() => void) | null = null; /** Fehlerklasse für API-Antworten */ export class ApiError extends Error { constructor( public status: number, message: string, ) { super(message); this.name = 'ApiError'; } } /** Gespeicherte Server-URL und Token aus den Preferences laden */ export async function initApi(): Promise { serverUrl = (await Preferences.get({ key: 'serverUrl' })).value ?? ''; token = (await Preferences.get({ key: 'token' })).value ?? ''; } /** Callback für abgelaufene Sitzung registrieren */ export function setAuthFailureHandler(fn: () => void): void { onAuthFailure = fn; } export function getServerUrl(): string { return serverUrl; } export async function setServerUrl(url: string): Promise { serverUrl = url.replace(/\/+$/, ''); await Preferences.set({ key: 'serverUrl', value: serverUrl }); } export function isLoggedIn(): boolean { return token !== ''; } async function setToken(value: string): Promise { token = value; await Preferences.set({ key: 'token', value }); } export async function clearToken(): Promise { token = ''; await Preferences.remove({ key: 'token' }); } /** Vollständige URL für einen API-Pfad bauen */ function url(endpoint: string): string { return `${serverUrl}${API_PATH}/${endpoint}`; } /** * Generischer Request mit Timeout und JSON-Verarbeitung. */ async function request(endpoint: string, options: RequestInit = {}): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); const headers: Record = { Accept: 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; if (options.body) headers['Content-Type'] = 'application/json'; let res: Response; try { res = await fetch(url(endpoint), { ...options, headers, signal: controller.signal }); } catch (e) { clearTimeout(timer); if (e instanceof DOMException && e.name === 'AbortError') { throw new ApiError(0, 'Zeitüberschreitung — Server nicht erreichbar'); } throw new ApiError(0, 'Keine Verbindung zum Server'); } clearTimeout(timer); if (res.status === 401) { await clearToken(); onAuthFailure?.(); throw new ApiError(401, 'Sitzung abgelaufen, bitte neu anmelden'); } let data: unknown = null; const text = await res.text(); if (text) { try { data = JSON.parse(text); } catch { throw new ApiError(res.status, 'Ungültige Server-Antwort'); } } if (!res.ok) { const msg = data && typeof data === 'object' && 'error' in data ? String((data as { error: unknown }).error) : `Fehler ${res.status}`; throw new ApiError(res.status, msg); } return data as T; } /* ----------------------------------------------------------------------- */ /* API-Methoden */ /* ----------------------------------------------------------------------- */ export interface LoginResult { token: string; expiresIn: number; user: { id: number; login: string; name: string; email: string; canWrite: boolean }; } /** Anmelden — speichert das Token bei Erfolg */ export async function login(loginName: string, password: string): Promise { const res = await request('auth.php', { method: 'POST', body: JSON.stringify({ login: loginName, password }), }); await setToken(res.token); return res; } /** Aufträge laden — nur aktive (offene), optional Suchtext */ export function listOrders(opts: { open?: boolean; q?: string } = {}): Promise<{ orders: Order[] }> { const p = new URLSearchParams(); if (opts.open) p.set('open', '1'); if (opts.q) p.set('q', opts.q); return request<{ orders: Order[] }>(`orders.php?${p.toString()}`); } /** Einzelnen Auftrag mit Protokollen laden */ export function getOrder(id: number): Promise<{ order: Order; protocols: unknown[] }> { return request(`orders.php?id=${id}`); } /** Kunden suchen */ export function searchCustomers(q: string): Promise<{ customers: Customer[] }> { return request<{ customers: Customer[] }>(`customers.php?q=${encodeURIComponent(q)}`); } /** Kurzform eines Protokolls, wie sie in der Kundenübersicht steht */ export interface ProtocolListItem { id: number; ref?: string; label?: string; clientUuid?: string; socId?: number | null; orderId?: number | null; /** Unix-Sekunden (nicht Millisekunden — so liefert es die API) */ dateDiag?: number; location?: string; subnet?: string; status?: number; deviceCount?: number; /** heißt serverseitig `measureCount` — nicht `measurementCount` (netdiag_api.lib.php) */ measureCount?: number; } /** Einzelnen Kunden mit Aufträgen und Protokollen laden */ export function getCustomer( id: number, ): Promise<{ customer: Customer; orders: Order[]; protocols: ProtocolListItem[] }> { return request(`customers.php?id=${id}`); } /** Ein Gerät, wie es der Server im Protokoll liefert */ export interface ServerDevice { id: number; ip: string; mac?: string | null; hostname?: string | null; netbiosName?: string | null; mdnsName?: string | null; mdnsServices?: string[]; customName?: string | null; vendor?: string | null; deviceType?: string | null; openPorts?: number[]; foundVia?: string | null; lastSeen?: number | null; note?: string | null; } /** * Ein Protokoll vom Server laden — samt Geräten und Messungen. * * Gebraucht vom Geräte-Vergleich, der die Liste des letzten Besuchs braucht. * Bewusst NICHT in den lokalen Bestand übernommen: das frühere Protokoll wird * nur gelesen, nie überschrieben. */ export function getProtocol( id: number, ): Promise<{ protocol: Record; devices: ServerDevice[]; measurements: unknown[] }> { return request(`protocols.php?id=${id}`); } export interface SyncResult { ok: boolean; protocolId: number; ref: string; created: boolean; pdfGenerated: boolean; } /** Protokoll zum Server synchronisieren (idempotent über clientUuid) */ export function syncProtocol(protocol: Protocol): Promise { const payload = { action: 'sync', protocol: { clientUuid: protocol.clientUuid, label: protocol.label, socId: protocol.socId ?? null, orderId: protocol.orderId ?? null, dateDiag: protocol.dateDiag, location: protocol.location, subnet: protocol.subnet, status: protocol.status, note: protocol.note, devices: protocol.devices.map((d) => ({ clientId: d.clientId, ip: d.ip, mac: d.mac ?? '', hostname: d.hostname ?? '', vendor: d.vendor ?? '', deviceType: d.deviceType ?? '', note: d.note ?? '', // Diese Merkmale ermittelt der IP-Scan, sie blieben aber bisher auf // dem Gerät: im Kundenprotokoll stand nur die IP, während der // Techniker in der App „Drucker HP, Port 9100" sah. netbiosName: d.netbiosName ?? '', mdnsName: d.mdnsName ?? '', mdnsServices: d.mdnsServices ?? [], customName: d.customName ?? '', openPorts: d.openPorts ?? [], foundVia: d.foundVia ?? '', lastSeen: d.lastSeen ?? null, })), measurements: protocol.measurements.map((m) => ({ deviceClientId: m.deviceClientId ?? null, tool: m.tool, category: m.category, label: m.label, params: m.params, result: m.result, measureStatus: m.measureStatus, dateMeasure: m.dateMeasure, })), }, }; return request('protocols.php', { method: 'POST', body: JSON.stringify(payload), }); } /** URL zum Protokoll-PDF (inkl. Token als Query-Parameter) */ export function pdfUrl(serverProtocolId: number): string { return `${serverUrl}${API_PATH}/pdf.php?id=${serverProtocolId}&jwt=${encodeURIComponent(token)}`; } export interface DebugLogUpload { entries: { ts: number; level: string; msg: string }[]; appVersion: string; device: string; } /** Debug-Log-Einträge zum Server übertragen (Endpoint applog.php) */ export function uploadDebugLog(payload: DebugLogUpload): Promise<{ ok: boolean; stored: number }> { return request<{ ok: boolean; stored: number }>('applog.php', { method: 'POST', body: JSON.stringify(payload), }); } /** * Neueste verfügbare App-Version erfragen. Der Server (update.php) prüft die * Registry — die App muss die private Registry nicht selbst erreichen. */ export function checkAppUpdate(): Promise<{ version: string | null }> { return request<{ version: string | null }>('update.php'); } /** Authentifizierte Download-URL der aktuellsten APK (über den Server-Proxy) */ export function updateDownloadUrl(): string { return `${serverUrl}${API_PATH}/update.php?download=1&jwt=${encodeURIComponent(token)}`; }