industrial-iot-dashboard
Industrial IoT dashboard: OPC UA to Kafka to WebSocket, rendered live
The presentation layer of an industrial telemetry pipeline, rendering 3-phase power analytics, breaker and digital I/O state, and a threshold alarm log from either a live WebSocket stream or a built-in simulator.
- Role
- Solo project. Frontend architecture, WebSocket client, data transformation layer, and the offline simulator.
- Stack
- React 19
- TypeScript
- Vite 7
- TailwindCSS 3
- Recharts 3
- OPC UA simulator
- Kafka: sensor-data
- kafka-ws-bridge
- WebSocket /ws
- Dashboard
Read left to right: each stage feeds the next. The prose sections below describe the same pipeline in full.
Problem
- Industrial telemetry is only useful when an operator can see it: 3-phase voltage, current and power, breaker and protection state, and which thresholds were crossed.
- Building that view normally requires access to a live plant, or a replay of plant data, before any interface work can start.
Constraints
- The dashboard is the last stage of a three-part pipeline, so its message contract is dictated by the upstream bridge rather than chosen freely.
- It has to be demonstrable with no backend at all, otherwise the interface cannot be reviewed or shown on its own.
- The live transport is a WebSocket carrying a continuous stream, so the client must handle connection loss, subscription, and constant re-rendering.
- Incoming payloads are flat maps of OPC tag names, not tidy nested objects, so a transformation layer is mandatory.
Architecture and data flow
- Edge: the OPC UA simulator publishes sensor values to the sensor-data Kafka topic.
- Transport: kafka-ws-bridge subscribes to that topic and broadcasts each message to subscribed WebSocket clients under /ws/sensor-data.
- Presentation: this app connects to ws://localhost:8000/ws with the sensor-data topic and renders the payloads it receives.
- A single header toggle switches the entire component tree between the live WebSocket source and the built-in simulator.
- The transformation layer decodes flat OPC tags such as OPC_PM_AMC_ACREL:UA_V, IA and PA into typed 3-phase readings, and tags such as OPC_BoardIO:CLS_F5_M3_DI_1 into digital I/O and protection state.
Engineering decisions
- The wire contract is typed in one place as WsEnvelope, WsClientCommand, SensorData, DigitalIOStatus and AlarmEvent, so both the live and simulated sources produce identical shapes and no component has to branch on where the data came from.
- Messages are filtered on the envelope event name, so connection bookkeeping frames and data frames share one socket without leaking into the UI.
- Blob and text frames are both handled, because the bridge may deliver payloads either way.
- The client auto-reconnects after a short delay instead of surfacing a dead socket, since a monitoring screen is expected to recover by itself.
- The simulator is a first-class mode rather than a fixture file, because it doubles as the standalone demo.
Testing and verification
- The production build runs the TypeScript compiler before Vite, so a type error fails the build rather than shipping.
- Honest gap: there are no automated tests. The only automated checks are the type-check and lint, and no CI workflow runs them on push.
- The simulation itself is the practical verification: mock mode exercises the same transformation and rendering path as live mode.
- An error path in the transformation layer is guarded because the payload shape from the simulator was still evolving during development, which is documented in the code.
What is proven today
- A working dashboard covering 3-phase power analytics, digital I/O with protection trips, and a threshold alarm log.
- Runs standalone through mock mode, and against the real pipeline through the WebSocket mode.
- What is not claimed: no measured latency, message rate, or scale figure, because none is published. The repository also still carries prototype leftovers and the package name temp-app, which is not presented here as finished product quality.
Source and references
Every claim on this page is traceable to the public repository, its tests, or its build output. Unverifiable numbers are deliberately omitted.