Universelle Laborplattform für Lasersysteme & optische Messtechnik.
LTor ist eine industriell gestaltete Desktop-Anwendung (Electron + React + TypeScript), die heterogene Laborhardware – SPSen, Sensoren, Messgeräte – über OPC UA, MQTT und REST in einer einheitlichen Oberfläche zusammenführt, Telemetrie live darstellt, Kamera- und Mikrofon-Signale der Workstation einbindet und alle sicherheitsrelevanten Vorgänge in einem manipulationssicheren Audit-Log protokolliert.
Das Frontend ist transport-agnostisch aufgebaut: derselbe React-Code läuft sowohl in der Electron-Desktop-App (lokal, direkt am Labor-PC) als auch im Webbrowser gegen einen Remote-Backend-Server (z. B. VPS) – siehe Abschnitt Deployment-Varianten.
| Bereich | Kern |
|---|---|
| Plattform | Windows / macOS / Linux (Electron) |
| Protokolle | OPC UA (node-opcua), MQTT (mqtt.js), REST (axios) |
| Sensoren | Kamera & Mikrofon der Workstation (Web MediaDevices API) |
| UI | React + TypeScript + TailwindCSS, einklappbare Panels, modul-basiert |
| Persistenz | Verbindungen → connections.json, Audit-Log → audit.jsonl |
| Audit-Log | Append-only JSON Lines, lückenlos, exportierbar |
| Transport | Lokal: Electron-IPC (window.ltor) · Remote-fähig: WebSocket-JSON-RPC |
Aktuell liegt die App im Quellzustand vor; Binaries werden in einem späteren
Release-Schritt via electron-builder paketiert.
Lokal starten:
git clone <repo-url> LTor
cd LTor
npm install
npm run dev # öffnet das Electron-FensterHinweis:
npm run devöffnet automatisch das Electron-Fenster. Daneben startet ein Vite-Dev-Server unterhttp://localhost:5173/– das ist nur ein Asset-Server für den Renderer und in dieser Konfiguration nicht zur direkten Browser-Benutzung gedacht (außer du betreibst zusätzlich ein Backend, siehe Deployment-Varianten).
Beim allerersten Start ist die Verbindungsliste leer. Empfohlener Ablauf:
- Sidebar → Verbindungen öffnen.
- Protokolltyp wählen (OPC UA / MQTT / REST), auf Neu klicken.
- Formular ausfüllen (siehe Konfiguration pro Protokoll) und speichern.
- In der Tabelle bei der Verbindung auf Verbinden klicken.
- Telemetrie-Modul öffnen → Live-Werte erscheinen als Sparklines.
- Sensoren (Kamera/Mikrofon) und Audit-Log verhalten sich selbsterklärend.
| Modul | Inhalt |
|---|---|
| Übersicht | Stat-Cards (verbundene Geräte, aktive Kanäle, Sensoren), kompakte Verbindungs-Status-Tabelle, jüngste Warnungen. |
| Telemetrie | Live-Sparklines pro Kanal, Werteanzeige, Min/Max/Last, Pause-Funktion, Filter & Verbindungs-Auswahl. |
| Verbindungen | CRUD für Verbindungen, pro Verbindung Connect/Disconnect/Edit/Delete. Formular je Protokolltyp. |
| Sensoren | Bordeigene Kamera (Vorschau) und Mikrofon (RMS-Pegelmeter). Auswahl des Geräts, Start/Stopp. |
| Audit-Log | Tabelle aller Ereignisse, Filter nach Kategorie/Zeitraum/Volltext, JSONL-Export, kompletter Reset. |
-
Endpoint:
opc.tcp://host:4840 -
Security Mode:
None,Sign,SignAndEncrypt -
Security Policy:
None,Basic256Sha256,Aes128_Sha256_RsaOaep,Aes256_Sha256_RsaPss -
Benutzer/Passwort: optional; leer = anonymous.
-
Publishing-Intervall (ms): Subscriptions-Default für alle Nodes.
-
Datenpunkte: eine Zeile pro Node im Format
nodeId|kanalNameBeispiel:
ns=2;s=Demo.Static.Scalar.Double|temperatur ns=2;s=Demo.Dynamic.Scalar.Int32|count
-
Broker-URL:
mqtt://,mqtts://oderws://…(Web-MQTT). -
Client-ID / Benutzer / Passwort: optional.
-
Topics: eine Zeile pro Subscription:
topic|kanalName|qosBeispiel:
lab/+/temperatur|temperatur|0 factory/#|allgemein|1Wildcards
+und#werden unterstützt.
-
Base-URL: z. B.
http://gateway.local:8000 -
Methode: GET / POST / PUT / PATCH / DELETE
-
Header: zeilenweise
Key: Value. -
Polling-Endpoints: zeilenweise
pfad|kanalName|intervallMs|jsonPathjsonPathist optional und bedient eine kleine Dollar-Subset-Syntax ($.path.to.value); leer = ganze Response wird verwendet.Beispiel:
/api/sensor|temperatur|1000|$.value /api/health|status|5000|
- Alle Seitenpanels (Titlebar, Sidebar, Tools-Panel, Statusbar) sind über die kleinen Chevron-Buttons einzeln ein-/ausklappbar. Eingeklappte Panels hinterlassen schmale Rand-Toggles, mit denen sie wieder geöffnet werden.
- DevTools öffnen sich standardmäßig nicht. Aktivierung wahlweise per:
- Flag:
npm run dev -- -- --devtools - Env-Variable:
LTOR_DEVTOOLS=1 - Tastenkürzel im Fenster:
F12bzw.Ctrl + Shift + I
- Flag:
LTor schreibt in das OS-typische userData-Verzeichnis:
| OS | Pfad |
|---|---|
| Windows | %APPDATA%\LTor\ (z. B. C:\Users\<Name>\AppData\Roaming\LTor\) |
| macOS | ~/Library/Application Support/LTor/ |
| Linux | ~/.config/LTor/ |
Dateien:
connections.json– alle Verbindungen samt Konfiguration.audit.jsonl– ein JSON-Objekt pro Zeile, append-only.
Beide Dateien sind menschen-lesbar und können bei Bedarf gesichert oder versioniert werden.
- Im Browser-Tab
http://localhost:5173/ist alles leer. Das ist gewollt: der Tab ist nur Asset-Server für den Renderer. Nutze stattdessen das Electron-Fenster – oder stelle einen Remote-Backend bereit (Deployment-Varianten). - App startet mit
ERR_REQUIRE_ESM: hexy. Sicherstellen, dassnpm installdie impackage.jsonfestgeschriebene Override"hexy": "0.3.5"aufgelöst hat (gefolgt von einem frischennode_modules-Build). - OPC-UA-Verbindung schlägt mit Zertifikatsfehler fehl. Security Mode auf
Nonesetzen oder die UA-Server-Endpunkt-CA im OS-Trust-Store hinterlegen. - MQTT bleibt auf „verbindet …“ hängen. Brokerseitig prüfen, ob die
Client-ID nicht doppelt vergeben ist; bei
mqtts://Zertifikate prüfen. - Mikrofon zeigt keinen Pegel. Browser-/OS-Berechtigung erteilen, anschließend Geräte neu laden klicken.
- Electron 32, electron-vite 2 (Bundling für Main / Preload / Renderer).
- TypeScript 5 (strict).
- React 18 + Zustand (State).
- TailwindCSS 3 + eigene Design-Tokens (
tailwind.config.js,index.css). - Recharts für Sparklines.
- node-opcua 2.x (OPC UA), mqtt.js 5 (MQTT), axios 1 (REST).
- lucide-react für Icons.
src/
├── shared/ # gemeinsame Typen (Connection, AuditEntry, IPC-Konstanten)
│ └── types.ts
│
├── main/ # Electron Hauptprozess (Node)
│ ├── index.ts # Bootstrap, BrowserWindow, IPC-Handler
│ ├── audit/auditLog.ts # JSONL Audit-Log (append-only)
│ ├── store/connectionStore.ts # Persistierung der Verbindungen
│ └── protocols/
│ ├── adapter.ts # ProtocolAdapter-Interface + AdapterEmitter
│ ├── manager.ts # Orchestriert mehrere Adapter parallel
│ ├── opcuaAdapter.ts # OPC UA (Subscriptions + Write)
│ ├── mqttAdapter.ts # MQTT (Wildcards + QoS)
│ └── restAdapter.ts # REST (Per-Endpoint-Polling + jsonPath)
│
├── preload/ # Context-Bridge
│ └── index.ts # exposeInMainWorld("ltor", api)
│
└── renderer/ # React-UI (Vite)
└── src/
├── App.tsx # Layout, Modul-Routing, Event-Abos
├── main.tsx # Entry, ReactDOM.render
├── index.css # Tailwind-Basis + Design-Tokens
├── ipc/ # Transport-Layer (Electron / WebSocket / null)
│ ├── transport.ts # LtorTransport-Interface
│ ├── electronTransport.ts
│ ├── wsTransport.ts # WebSocket-JSON-RPC-Client
│ ├── nullTransport.ts # Fallback, wenn kein Backend verfügbar
│ └── index.ts # ipc = resolveTransport()
├── store/ # Zustand-Stores (app/connections/telemetry/audit/sensors)
├── components/layout/ # TitleBar, Sidebar, StatusBar, ToolsPanel, PanelToggleBar
├── modules/ # Fachliche Module
│ ├── dashboard/
│ ├── connections/
│ ├── telemetry/
│ ├── sensors/
│ └── audit/
└── types/ipc.d.ts # window.ltor Typdeklaration (für Electron)
- Node.js ≥ 20
- npm ≥ 10
- (Windows) Visual Studio Build Tools werden ggf. von
node-opcuabenötigt.
npm install # 1× nach jedem Pull
npm run dev # Watch-Mode, öffnet Electron-Fenster
npm run dev -- -- --devtools # zusätzlich DevTools öffnen
npm run typecheck # tsc -p tsconfig.node.json && tsc -p tsconfig.web.json
npm run build # Produktions-Bundles in out/{main,preload,renderer}/
npm run start # Vorschau des Production-Builds (electron-vite preview)LTor folgt der klassischen Electron-3-Prozess-Trennung:
┌────────────────────────────┐ ┌───────────────────────────┐
│ Main-Prozess (Node) │ IPC ⇄ │ Preload (Context-Bridge) │
│ - ProtocolManager │ │ - exposeInMainWorld(ltor) │
│ - OPC UA / MQTT / REST │ └───────────────────────────┘
│ - AuditLog (JSONL) │ ↑ window.ltor
│ - ConnectionStore │ │
└────────────────────────────┘ │
│
┌──────────────┴───────────────────┐
│ Renderer (React, Vite, Tailwind)│
│ - Module + Layout-Shell │
│ - Zustand-Stores │
│ - ipc/* Transport-Abstraktion │
└──────────────────────────────────┘
- Sicherheit:
contextIsolation: true,nodeIntegration: false, Sandbox-konform; ausschließlich die explizit im Preload exponierteLtorApi-Oberfläche ist im Renderer sichtbar. - Live-Daten: Adapter pushen über
ProtocolManager.onSampleundonStatuszum Main-Prozess; dieser broadcastet viawebContents.sendan alle BrowserWindows.
Der Renderer redet niemals direkt mit window.ltor, sondern stets über die
Abstraktion in src/renderer/src/ipc/:
import { ipc } from "../ipc";
await ipc.connections.list();
const off = ipc.connections.onTelemetry(sample => …);ipc/index.ts löst zur Laufzeit den passenden Transport auf:
window.ltorvorhanden →ElectronTransport(Standard im Desktop-Modus).import.meta.env.VITE_LTOR_WS_URLgesetzt →WebSocketTransportzur angegebenen URL (z. B.wss://lab.example.com/ltor).- Browser ohne Vite-Dev-Port → WebSocket zu
ws[s]://<host>/ltor(Same-Host). - Sonst →
NullTransport, der alle Calls mit verständlicher Fehlermeldung ablehnt; der UI-State (Loading/Error) zeigt das sauber.
Der aktuelle Modus wird unten rechts in der Statusbar als Badge dargestellt
(Desktop, Remote, Offline).
Ein minimales JSON-RPC reicht. Ein eigenes Backend muss nur das implementieren:
Unterstützte Methoden spiegeln die LtorTransport-Schnittstelle wider
(sys.info, audit.list/append/clear,
connections.list/create/update/delete/connect/disconnect/write).
-
Konfig-Typ in
src/shared/types.tshinzufügen (type ConnectionKind, Config-Interface, Union-Erweiterung desConnection["config"]). -
Adapter in
src/main/protocols/<name>Adapter.tsanlegen, derProtocolAdapterimplementiert undAdapterEmittererweitert:export class FooAdapter extends AdapterEmitter implements ProtocolAdapter { readonly kind = "foo" as const; constructor(public readonly id: string, private cfg: FooConfig) { super(); } async connect(): Promise<void> { this.emitStatus("connecting"); // …connect… this.emitStatus("connected"); // bei jedem Sample: this.emitSample({ connectionId: this.id, channel: "x", value: 42, ts: Date.now() }); } async disconnect(): Promise<void> { this.emitStatus("disconnected"); } async write(channel: string, value: number | string | boolean): Promise<void> { … } }
-
Im ProtocolManager (
src/main/protocols/manager.ts) den neuenkindiminstantiate()-Switch registrieren. -
Im Renderer ein Formularfeld in
ConnectionForm.tsxergänzen und inConnectionsModule.tsx#endpointSummarydie Anzeige der wichtigsten Konfig-Eigenschaft. -
Optional: Default-Werte in
defaultConfig().
- Datei
src/renderer/src/modules/<name>/<Name>Module.tsxerstellen. - In
src/renderer/src/store/appStore.tsdas neueModuleIdergänzen. - In
src/renderer/src/components/layout/Sidebar.tsxeinenNAV-Eintrag. - In
src/renderer/src/App.tsxdas Modul ins Routing (activeModule === …) einbauen.
State innerhalb des Moduls → eigener Zustand-Store in store/. Datenzugriff
ausschließlich via ipc (Transport-Abstraktion) – nie direkt über
window.ltor.
audit.jsonl ist eine append-only-Datei. Jeder Eintrag:
{
"id": "01H…", // ULID (chronologisch sortierbar)
"ts": 1731585032123, // ms epoch
"category": "connection", // info | warning | alarm | sensor | connection | system
"actor": "user", // user | system
"action": "connection_created",
"details": {
/* frei strukturierbar */
},
}Schreibzugriff erfolgt ausschließlich über AuditLog.append() (oder den
IPC-Endpunkt audit:append). Manipulation an der Datei zerstört die
Lückenlosigkeits-Garantie und sollte im Produktivbetrieb durch OS-ACLs
verhindert werden.
Eine neue Kategorie:
- In
src/shared/types.tszur UnionAuditEntry["category"]hinzufügen. - Optional Filter im
AuditLogModule.tsxergänzen.
npm run build
# → out/main/, out/preload/, out/renderer/
# Verteilung als Electron-Bundle (electron-builder folgt später).Konzept: der bestehende Electron-Main wird in einen schlanken Node-Server
extrahiert, der das gleiche IPC-Protokoll via WebSocket bedient
(src/renderer/src/ipc/wsTransport.ts beschreibt das Wire-Format).
-
Backend (eigenes Repo /
server/):- Express oder Fastify +
ws. - Wiederverwendet
src/main/protocols/*(sind reines Node) undsrc/main/audit/*1:1. - Implementiert die in WebSocket-Wire-Protokoll gelisteten Methoden + Events.
- Express oder Fastify +
-
Frontend bauen mit gesetzter Backend-URL:
VITE_LTOR_WS_URL=wss://lab.example.com/ltor \ npx vite build --base=/ \ --config electron.vite.config.ts \ # (für reines Web-Deployment Vite-Config geringfügig anpassen, # electron-Plugin entfällt)
-
Hosting: statisches
dist/-Verzeichnis hinter Nginx ausliefern, das/ltorals WebSocket-Reverse-Proxy an den Node-Backend forwarded.Kamera/Mikrofon-Sensoren funktionieren unverändert, da sie browser-natives
MediaDevicesnutzen.
Der NullTransport sorgt dafür, dass das UI auch ohne erreichbares Backend
verständliche Fehlermeldungen statt Hänger zeigt.
- Phase 3: KI-Assistenzen (Anomalie-Erkennung, Wartungs-Hints).
- Phase 4: MES-/ERP-Integration, multimodale Auswertungen.
- Längerfristig: electron-builder-Paketierung (Signiert), Plugin-API für Drittprotokolle, Mehrbenutzer-Rollen, persistente Telemetrie-Aufzeichnung.
GNU General Public License 3
