Plast Track MVP
Lightweight MES/IoT MVP for supervising multi-brand injection molding machines using passive signal acquisition, MQTT event ingestion, operator input, and real-time web dashboards.
Objective
This MVP supervises production without Euromap dependency and without sending any command back to the press. It focuses on:
- machine status visualization
- automatic cycle counting
- real cycle time capture
- current production order tracking
- produced quantity and scrap quantity
- automatic downtime detection
- operator downtime qualification
- scrap declaration
- daily OEE/TRS
- workshop dashboard in real time
Architecture
Passive machine signal / external sensor
-> isolated relay / optocoupler / industrial DI module
-> IoT gateway or edge service
-> MQTT topic
-> FastAPI backend
-> PostgreSQL
-> WebSocket / REST API
-> React frontend dashboard
Services:
database: PostgreSQL with schema and seed datamqtt: Mosquitto brokerbackend: FastAPI API, MQTT consumer, downtime logic, OEE computation, WebSocket notificationsedge-simulator: publishes simulated machine events on MQTTfrontend: React + TypeScript operator/dashboard interfacesimulator-ui: separate React + TypeScript test console for sending simulated events
Repository Layout
/backend
/frontend
/simulator-ui
/edge-simulator
/database/init.sql
/database/seed.sql
/mqtt/mosquitto.conf
/docker-compose.yml
Run Locally
Prerequisites:
- Docker Desktop or Docker Engine with Compose
Start the full MVP:
docker compose up --build
Exposed services:
- frontend:
http://localhost:5173 - simulator UI:
http://localhost:5174 - backend API:
http://localhost:8000 - backend health:
http://localhost:8000/health - PostgreSQL:
localhost:5432 - MQTT:
localhost:1883
Deploy With Portainer
Use a Portainer Stack from the Git repository. Do not paste only the Compose
file into the web editor, because the stack also needs these repository files:
database/init.sqldatabase/seed.sqlmqtt/mosquitto.confbackend/frontend/simulator-ui/edge-simulator/
1. Create The Stack
In Portainer:
- Go to
Stacks. - Click
Add stack. - Choose
Repository. - Set repository URL:
https://gitea.b2i.business/masterdev/plast-track.git
- Set branch:
main
- Set Compose path:
docker-compose.yml
2. Configure Stack Variables
For a local server accessed directly by IP, replace SERVER_IP with the host IP:
POSTGRES_DB=plasttrack
POSTGRES_USER=plasttrack
POSTGRES_PASSWORD=change-this-password
FRONTEND_ORIGIN=http://SERVER_IP:5173
FRONTEND_ORIGINS=http://SERVER_IP:5173,http://SERVER_IP:5174,http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174
BACKEND_PORT=8000
VITE_API_BASE_URL=http://SERVER_IP:8000
VITE_WS_URL=ws://SERVER_IP:8000/ws/dashboard
VITE_ALLOWED_HOSTS=SERVER_IP,localhost,127.0.0.1
SIMULATOR_PORT=5174
SIMULATOR_API_BASE_URL=http://SERVER_IP:8000
SIMULATOR_ALLOWED_HOSTS=SERVER_IP,localhost,127.0.0.1
For a domain with HTTPS and a reverse proxy, use:
POSTGRES_DB=plasttrack
POSTGRES_USER=plasttrack
POSTGRES_PASSWORD=change-this-password
FRONTEND_ORIGIN=https://plasttrack.example.com
FRONTEND_ORIGINS=https://plasttrack.example.com,https://simulator.plasttrack.example.com
BACKEND_PORT=8000
VITE_API_BASE_URL=https://api.plasttrack.example.com
VITE_WS_URL=wss://api.plasttrack.example.com/ws/dashboard
VITE_ALLOWED_HOSTS=plasttrack.example.com
SIMULATOR_PORT=5174
SIMULATOR_API_BASE_URL=https://api.plasttrack.example.com
SIMULATOR_ALLOWED_HOSTS=simulator.plasttrack.example.com
Important: do not keep localhost in VITE_API_BASE_URL or VITE_WS_URL
for a remote deployment. localhost would point to the operator's browser
machine, not the Docker host.
If Vite returns Blocked request. This host is not allowed, add the public
frontend host to VITE_ALLOWED_HOSTS as a comma-separated value.
If the backend host port conflicts with another service such as Portainer,
change BACKEND_PORT instead of editing docker-compose.yml. For example:
BACKEND_PORT=8001
VITE_API_BASE_URL=http://SERVER_IP:8001
VITE_WS_URL=ws://SERVER_IP:8001/ws/dashboard
SIMULATOR_API_BASE_URL=http://SERVER_IP:8001
If the browser shows a CORS error, make sure the exact frontend origin is in
FRONTEND_ORIGINS. Example:
FRONTEND_ORIGINS=https://plasttrack.sable.ynsdev.site,https://simulator.plasttrack.sable.ynsdev.site
If the frontend is served over HTTPS, the WebSocket URL must use wss://, not
ws://. Browsers block insecure WebSockets from secure pages. Recommended
same-domain reverse proxy variables:
FRONTEND_ORIGIN=https://plasttrack.sable.ynsdev.site
FRONTEND_ORIGINS=https://plasttrack.sable.ynsdev.site,https://simulator.plasttrack.sable.ynsdev.site
VITE_API_BASE_URL=https://plasttrack.sable.ynsdev.site
VITE_WS_URL=wss://plasttrack.sable.ynsdev.site/ws/dashboard
VITE_ALLOWED_HOSTS=plasttrack.sable.ynsdev.site,localhost,127.0.0.1
SIMULATOR_API_BASE_URL=https://plasttrack.sable.ynsdev.site
SIMULATOR_ALLOWED_HOSTS=simulator.plasttrack.sable.ynsdev.site,localhost,127.0.0.1
The main dashboard reverse proxy must route:
/api/* -> backend:8000
/ws/dashboard -> backend:8000
/* -> frontend:5173
The simulator must use a separate link, not a tab inside the operator frontend:
https://simulator.plasttrack.example.com -> simulator-ui:5174
If SIMULATOR_API_BASE_URL points to the main dashboard domain, that dashboard reverse proxy must also route /api/* to the backend.
3. Deploy
Click Deploy the stack.
Expected exposed ports:
- frontend:
5173 - simulator UI:
5174 - backend API:
8000 - MQTT:
1883 - MQTT WebSocket listener:
9001
PostgreSQL is intentionally not exposed by default in the Compose file. It is only reachable by the backend on the internal Docker network.
The stack builds small local images for database and mqtt so Portainer does
not need to bind-mount individual files like database/init.sql or
mqtt/mosquitto.conf. This avoids file-versus-directory mount errors in
Portainer Git stacks.
The backend receives PostgreSQL connection settings as separate variables
(POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD) and
builds the SQLAlchemy URL internally. This avoids broken database URLs when the
password contains special characters. The backend also retries the database
connection during startup to tolerate Portainer service/DNS startup delays.
4. Verify
Open:
http://SERVER_IP:5173
Open the simulator test console:
http://SERVER_IP:5174
Check the backend:
http://SERVER_IP:8000/health
Expected response:
{"status":"ok"}
5. Updating From Gitea
After pushing new commits to Gitea:
- Open the stack in Portainer.
- Click
Pull and redeployorUpdate the stack. - Enable rebuild if Portainer asks, because
backend,frontend, andedge-simulatorare built from the repository.
Demo Data
Seeded machines:
INJ-01- Haitian Mars IIINJ-02- Engel e-victoryINJ-03- Arburg Allrounder
Seeded production orders:
OF-1001running onINJ-01OF-1002planned onINJ-02
The edge simulator continuously publishes cycles and stop events for the demo machines.
MQTT Topics And Payloads
Topic pattern:
plasttrack/machines/{machine_code}/events
Raw signal event example:
{
"machine_id": "INJ-01",
"event_type": "digital_input_changed",
"timestamp": "2026-06-14T10:32:15Z",
"input_name": "cycle_signal",
"input_value": true,
"cycle_time_sec": 18.7,
"source": "digital_input"
}
The backend derives valid cycles from the configured signal edge and timing rules. It still stores normalized cycle_completed events if upstream integrations publish them directly.
Automatic stop event example:
{
"machine_id": "INJ-01",
"event_type": "machine_stopped",
"timestamp": "2026-06-14T10:35:20Z",
"auto_detected": true
}
REST API
Machines:
GET /api/dashboardGET /api/machinesGET /api/machines/{machine_id}GET /api/machines/{machine_id}/configPATCH /api/machines/{machine_id}/configGET /api/machines/{machine_id}/statusGET /api/machines/{machine_id}/eventsGET /api/machines/{machine_id}/cyclesGET /api/machines/{machine_id}/downtimes
Production orders:
GET /api/production-ordersPOST /api/production-ordersPOST /api/production-orders/{id}/startPOST /api/production-orders/{id}/pausePOST /api/production-orders/{id}/close
Downtimes:
GET /api/downtimes/openPOST /api/downtimes/{id}/qualify
Scrap:
GET /api/scrapsPOST /api/scraps
OEE:
GET /api/oee/daily?date=2026-06-14GET /api/oee/machines/{machine_id}?from=2026-06-14T00:00:00Z&to=2026-06-14T23:59:59Z
Reference data:
GET /api/reference-data
Simulator:
POST /api/simulator/eventsPOST /api/simulator/scenarios
WebSocket:
ws://localhost:8000/ws/dashboard
Manual API Checks
List machines:
curl http://localhost:8000/api/machines
Create a production order:
curl -X POST http://localhost:8000/api/production-orders \
-H "Content-Type: application/json" \
-d '{
"order_number": "OF-2001",
"machine_id": 2,
"article_ref": "ART-2001",
"article_name": "Bac Technique",
"mold_ref": "M-2001",
"material_ref": "PP-BLACK",
"planned_qty": 1200,
"cavities": 2,
"theoretical_cycle_time_sec": 21.5
}'
Qualify a downtime:
curl -X POST http://localhost:8000/api/downtimes/1/qualify \
-H "Content-Type: application/json" \
-d '{
"reason_code": "attente_matiere",
"comment": "Material not available at the machine",
"qualified_by": "operateur_1"
}'
Declare scrap:
curl -X POST http://localhost:8000/api/scraps \
-H "Content-Type: application/json" \
-d '{
"machine_id": "INJ-01",
"production_order_id": 1,
"quantity": 12,
"reason_code": "bavure",
"comment": "Flash on parting line",
"operator_name": "operateur_1"
}'
OEE Logic
Formula:
OEE = Availability x Performance x Quality
Implemented rules:
- planned time: overlap of started production orders with the query window
- downtime: overlap of machine downtimes with the query window
- operating time:
planned time - downtime - performance:
theoretical cycle contribution / operating time, capped at100% - quality:
good quantity / total produced quantity
How The Simulator Works
There are two simulator components:
edge-simulator: automatic MQTT publisher for continuous demo datasimulator-ui: separate browser console for manually triggering test cases
The edge simulator publishes:
digital_input_changedformachine_power_ondigital_input_changedforcycle_signaldigital_input_changedforgeneral_alarm
Each machine gets a nominal cycle time, a pulse width, and a random stop probability. The backend consumes these MQTT events, detects the configured cycle edge, and updates production data in PostgreSQL.
The separate simulator UI calls backend simulator endpoints and can trigger:
- power on/off
- single normalized cycle
- cycle burst
- passive
cycle_signaledge using each machine's configured edge - debounce noise that should be ignored
- forced stop / open downtime
- recovery with a new cycle
- alarm on/off
- raw passive digital input by name and value
Use the simulator UI through its own URL, for example:
http://localhost:5174
https://simulator.plasttrack.example.com
Connecting A Real Passive Gateway Later
To replace the simulator with a real gateway:
- Read passive, electrically isolated machine signals only.
- Publish normalized JSON events to the same MQTT topic pattern.
- Keep event timestamps in UTC ISO-8601 format.
- Map per-machine debounce, min/max cycle time, and stop detection delay in the database or future admin UI.
Expected minimum signals:
machine_power_oncycle_signal
Optional signals:
auto_modemold_openejector_forwardgeneral_alarmpump_running
Industrial Safety
- Never wire the IoT gateway directly to PLC outputs or machine circuits.
- Use interface relays, optocouplers, or isolated industrial input modules.
- Validate every wiring decision with a qualified automation or industrial electrical technician.
- Keep acquisition passive and non-intrusive.
- Do not modify the machine PLC logic for this MVP.
- This MVP must never command or pilot the press.
Tests
Backend tests included:
- OEE calculation unit test
- MQTT event parsing test
Run locally:
cd backend
pytest
Frontend build check:
cd frontend
npm install
npm run build
Simulator UI build check:
cd simulator-ui
npm install
npm run build
Current Limits
- No authentication or role management
- No production scheduling calendar
- No historian-grade buffering on the edge side
- No historical reporting endpoints beyond daily OEE/TRS
- No real passive gateway adapter package yet; the simulator publishes normalized MQTT payloads
- WebSocket currently pushes refresh notifications, then the UI refetches data
- The simulators model passive-signal-derived events, not actual electrical IO reads
Current UI Modules
The frontend is organized into module tabs rather than one long dashboard page:
Atelier: machine cards, status filtering, sorting, quick actions, fullscreen workshop modeMachine: selected machine detail, cycle trend, recent events, downtime historyTRS: daily OEE/TRS summary and per-machine OEEOF: production order creation and status managementArrets: open downtime qualificationRebuts: scrap declaration and scrap logConfig: persistent passive signal and timing configuration per machine
Operator forms include inline validation, inline API error feedback, and success toasts.
Industrial Readiness Backlog
Next hardening steps before a real shop-floor pilot:
- define a real passive gateway adapter spec with exact input mapping and MQTT payload examples
- add edge-side buffering for MQTT/backend outage periods
- expand tests for downtime transitions, order status transitions, and signal debounce edge cases
- add historical reporting endpoints for OEE, downtime, and scrap trends