An operations-center digital twin viewer for factory floors. A top-down SVG floor plan renders halls, production lines, and machine stations with live equipment status overlays; a simulation engine drives telemetry so the UI can be exercised without plant connectivity.
Browser (Next.js App Router, client-heavy)
├── app/ routes: / (viewer), /analytics,
│ /layout-editor, /settings, /help
├── state/
│ ├── SettingsContext persisted display prefs (localStorage),
│ │ drives theme + simulation cadence
│ └── FactoryContext layout selection, live telemetry store,
│ layer toggles, viewport/fly-to, toasts,
│ connection + acknowledgment state
├── lib/
│ ├── factory-data deterministic mock plant generator +
│ tickRuntime() simulation step
│ ├── idb IndexedDB cache (versioned envelope)
│ └── status status/color contracts
└── components/
├── canvas/ FloorPlanCanvas, MachineNode, ConveyorPath,
│ ZoneOverlay, error boundary, offline banner
├── tree/, panels/, ui/ asset tree, detail panel, legend, minimap,
│ toasts, widgets
└── toolbar/ top toolbar with search + layers
Data flows one way: the simulation engine produces immutable runtime snapshots
(Record<machineId, MachineRuntime>), React contexts distribute them, and SVG
components render pure functions of (layout, runtime, layers).
- Next.js 14 (App Router) + TypeScript strict mode
- Tailwind CSS design system (dark operations-center palette)
- Hand-rolled SVG rendering — no charting/SVG library
- WebSocket-shaped real-time layer: currently a deterministic simulator
(
tickRuntime); swapFactoryProvider's interval body for a WS subscription pointed atNEXT_PUBLIC_WS_URL - IndexedDB for offline floor-layout caching (schema-versioned envelope)
npm install
cp .env.example .env.local # optional; defaults work without it
npm run dev # http://localhost:3000
npm run test # Jest + React Testing Library
npm run lint && npm run typecheck && npm run buildinterface FactoryLayout {
id: string; // "FT01"
name: string; location: string;
viewBox: { w: 1600; h: 1000 };
halls: Hall[]; // { id, code, name, x, y, w, h, lineIds }
lines: ProductionLine[]; // { id, name, hallId, stationIds,
// conveyorD, connections: [fromStn,toStn][] }
stations: Station[]; // { id, name, lineId, machineId, x, y } <- conveyor anchor
machines: Machine[]; // { id, assetTag ("01-A1-03"), name, type,
// hallId, lineId, stationId, x, y, w, h,
// shape: "rect"|"circle",
// lastMaintenanceISO, maintenanceType,
// maintenanceIntervalDays,
// cycleTimeIdealS, notes: OperatorNote[] }
zones: ZoneOverlayDef[]; // { id, label, kind, hallId, x,y,w,h, tempC }
}Live runtime values live in a parallel map:
interface MachineRuntime {
status: "online" | "warning" | "offline" | "fault";
oeePct; temperatureC; vibrationMms; flowRateLpm; capacityPct;
partsProduced; cycleTimeActualS;
trends?: { temperatureC; vibrationMms }; // previous tick, powers arrows
}The floor plan uses a fixed 1600 × 1000 unit viewBox ("floor units"),
independent of pixels. Halls are 480-wide blocks at x = 40 / 560 / 1080;
within a hall, lines are horizontal bands and stations are spaced evenly along
each conveyor row, alternating above/below the belt. The <svg> stretches this
viewBox to its container (responsive down to ~1024px), so all machine
coordinates in the JSON are stable across screen sizes. Zoom/pan is implemented
purely by animating the SVG viewBox — no element transforms.
tickRuntime(layout, runtime, rand = Math.random) picks 1–2 random machines
per tick, regenerates their telemetry from status-weighted ranges, accumulates
partsProduced, records pre-tick temperature/vibration as trends, and
returns an immutable snapshot. FactoryProvider runs it on an interval driven
by Settings → Auto-refresh (3/5/10/30s), diffs old vs new statuses through
faultTransitions, filters by the notification preference, raises toast
alerts (ALERT: <tag> has entered FAULT state), and simulates occasional
gateway hiccups that pause the feed behind a reconnect banner.
rand is injectable for deterministic tests.
npm run build && npm start # Node server
# or export to any static host with a WebSocket-capable edge:
# set NEXT_PUBLIC_WS_URL then `next build`Environment variables (see .env.example):
NEXT_PUBLIC_WS_URL— WebSocket endpoint for the live gateway. When unset, the built-in simulator drives updates.
Health checks for an ops deployment should hit / and expect HTTP 200 plus
/manifest.webmanifest.