From e3d8f856e38d99a8fc0ec3d0c8a22a5c8140d83c Mon Sep 17 00:00:00 2001 From: masterdev Date: Wed, 17 Jun 2026 01:06:21 +0100 Subject: [PATCH] Add separate simulator UI --- .gitignore | 6 + README.md | 79 +- backend/app/config.py | 2 +- backend/app/main.py | 168 ++++- backend/app/schemas.py | 18 + backend/tests/test_events.py | 42 +- docker-compose.yml | 13 +- simulator-ui/Dockerfile | 12 + simulator-ui/index.html | 12 + simulator-ui/package-lock.json | 1241 ++++++++++++++++++++++++++++++++ simulator-ui/package.json | 22 + simulator-ui/src/main.tsx | 353 +++++++++ simulator-ui/src/styles.css | 370 ++++++++++ simulator-ui/src/vite-env.d.ts | 1 + simulator-ui/tsconfig.json | 17 + simulator-ui/vite.config.ts | 19 + 16 files changed, 2361 insertions(+), 14 deletions(-) create mode 100644 simulator-ui/Dockerfile create mode 100644 simulator-ui/index.html create mode 100644 simulator-ui/package-lock.json create mode 100644 simulator-ui/package.json create mode 100644 simulator-ui/src/main.tsx create mode 100644 simulator-ui/src/styles.css create mode 100644 simulator-ui/src/vite-env.d.ts create mode 100644 simulator-ui/tsconfig.json create mode 100644 simulator-ui/vite.config.ts diff --git a/.gitignore b/.gitignore index 3481f72..18c4280 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,12 @@ frontend/build/ frontend/coverage/ frontend/.vite/ frontend/*.tsbuildinfo +simulator-ui/node_modules/ +simulator-ui/dist/ +simulator-ui/build/ +simulator-ui/coverage/ +simulator-ui/.vite/ +simulator-ui/*.tsbuildinfo npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/README.md b/README.md index b9fde1b..e094d54 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,14 @@ Services: - `backend`: FastAPI API, MQTT consumer, downtime logic, OEE computation, WebSocket notifications - `edge-simulator`: publishes simulated machine events on MQTT - `frontend`: React + TypeScript operator/dashboard interface +- `simulator-ui`: separate React + TypeScript test console for sending simulated events ## Repository Layout ```text /backend /frontend +/simulator-ui /edge-simulator /database/init.sql /database/seed.sql @@ -65,6 +67,7 @@ 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` @@ -80,6 +83,7 @@ file into the web editor, because the stack also needs these repository files: - `mqtt/mosquitto.conf` - `backend/` - `frontend/` +- `simulator-ui/` - `edge-simulator/` ### 1. Create The Stack @@ -116,11 +120,14 @@ POSTGRES_DB=plasttrack POSTGRES_USER=plasttrack POSTGRES_PASSWORD=change-this-password FRONTEND_ORIGIN=http://SERVER_IP:5173 -FRONTEND_ORIGINS=http://SERVER_IP:5173,http://localhost:5173,http://127.0.0.1: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: @@ -130,11 +137,14 @@ POSTGRES_DB=plasttrack POSTGRES_USER=plasttrack POSTGRES_PASSWORD=change-this-password FRONTEND_ORIGIN=https://plasttrack.example.com -FRONTEND_ORIGINS=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` @@ -151,13 +161,14 @@ 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: ```env -FRONTEND_ORIGINS=https://plasttrack.sable.ynsdev.site +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 @@ -166,13 +177,15 @@ same-domain reverse proxy variables: ```env FRONTEND_ORIGIN=https://plasttrack.sable.ynsdev.site -FRONTEND_ORIGINS=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 reverse proxy must route: +The main dashboard reverse proxy must route: ```text /api/* -> backend:8000 @@ -180,6 +193,14 @@ The reverse proxy must route: /* -> frontend:5173 ``` +The simulator must use a separate link, not a tab inside the operator frontend: + +```text +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`. @@ -187,6 +208,7 @@ Click `Deploy the stack`. Expected exposed ports: - frontend: `5173` +- simulator UI: `5174` - backend API: `8000` - MQTT: `1883` - MQTT WebSocket listener: `9001` @@ -213,6 +235,12 @@ Open: http://SERVER_IP:5173 ``` +Open the simulator test console: + +```text +http://SERVER_IP:5174 +``` + Check the backend: ```text @@ -325,6 +353,11 @@ Reference data: - `GET /api/reference-data` +Simulator: + +- `POST /api/simulator/events` +- `POST /api/simulator/scenarios` + WebSocket: - `ws://localhost:8000/ws/dashboard` @@ -400,7 +433,12 @@ Implemented rules: ## How The Simulator Works -The simulator publishes: +There are two simulator components: + +- `edge-simulator`: automatic MQTT publisher for continuous demo data +- `simulator-ui`: separate browser console for manually triggering test cases + +The edge simulator publishes: - `digital_input_changed` for `machine_power_on` - `digital_input_changed` for `cycle_signal` @@ -408,6 +446,25 @@ The simulator publishes: 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_signal` edge 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: + +```text +http://localhost:5174 +https://simulator.plasttrack.example.com +``` + ## Connecting A Real Passive Gateway Later To replace the simulator with a real gateway: @@ -461,6 +518,14 @@ npm install npm run build ``` +Simulator UI build check: + +```bash +cd simulator-ui +npm install +npm run build +``` + ## Current Limits - No authentication or role management @@ -469,7 +534,7 @@ npm run build - 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 simulator models passive-signal-derived events, not actual electrical IO reads +- The simulators model passive-signal-derived events, not actual electrical IO reads ## Current UI Modules diff --git a/backend/app/config.py b/backend/app/config.py index a936d3e..ad65719 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -19,7 +19,7 @@ class Settings(BaseSettings): mqtt_topic: str = "plasttrack/machines/+/events" mqtt_client_id: str = "plast-track-backend" frontend_origin: str | None = None - frontend_origins: str = "http://localhost:5173,http://127.0.0.1:5173" + frontend_origins: str = "http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174" downtime_check_interval_sec: int = 5 database_startup_retries: int = 30 database_startup_retry_delay_sec: float = 2.0 diff --git a/backend/app/main.py b/backend/app/main.py index b057120..7b214bb 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio from contextlib import asynccontextmanager -from datetime import date, datetime, timezone +from datetime import date, datetime, timedelta, timezone from fastapi import Depends, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware @@ -13,9 +13,17 @@ from sqlalchemy.orm import Session from .config import get_settings from .database import Base, SessionLocal, engine, get_db from .models import Cycle, Downtime, DowntimeReason, Machine, MachineEvent, ProductionOrder, Scrap, ScrapReason -from .schemas import DowntimeQualify, MachineConfigUpdate, ProductionOrderCreate, ScrapCreate +from .schemas import ( + DowntimeQualify, + MachineConfigUpdate, + MachineEventIn, + ProductionOrderCreate, + ScrapCreate, + SimulatorEventCreate, + SimulatorScenarioCreate, +) from .services.dashboard import build_dashboard_snapshot, serialize_machine_card, serialize_machine_config -from .services.events import detect_missing_cycles, get_open_downtime, get_running_order +from .services.events import detect_missing_cycles, get_open_downtime, get_running_order, handle_machine_event from .services.mqtt_listener import MqttListener from .services.oee import compute_daily_oee, compute_machine_oee from .services.realtime import hub @@ -259,6 +267,160 @@ def get_machine_events(machine_id: int, limit: int = Query(default=50, le=200), ] +def _get_machine_by_code_or_404(db: Session, machine_code: str) -> Machine: + machine = db.execute(select(Machine).where(Machine.code == machine_code)).scalar_one_or_none() + if machine is None: + raise HTTPException(status_code=404, detail="Machine not found") + return machine + + +def _emit_simulator_event(db: Session, payload: SimulatorEventCreate) -> dict: + _get_machine_by_code_or_404(db, payload.machine_id) + incoming = MachineEventIn( + machine_id=payload.machine_id, + event_type=payload.event_type, + timestamp=payload.timestamp or datetime.now(UTC), + cycle_time_sec=payload.cycle_time_sec, + source="simulator-ui", + input_name=payload.input_name, + input_value=payload.input_value, + ) + result = handle_machine_event(db, incoming) + if result["should_refresh"]: + hub.notify({"type": "refresh", "source": "simulator-ui", **result}) + return result + + +@app.post("/api/simulator/events") +def create_simulator_event(payload: SimulatorEventCreate, db: Session = Depends(get_db)) -> dict: + return _emit_simulator_event(db, payload) + + +@app.post("/api/simulator/scenarios") +def run_simulator_scenario(payload: SimulatorScenarioCreate, db: Session = Depends(get_db)) -> dict: + machine = _get_machine_by_code_or_404(db, payload.machine_id) + now = datetime.now(UTC) + cycle_time_sec = float(payload.cycle_time_sec or 20) + events: list[SimulatorEventCreate] = [] + + if payload.scenario == "power_on": + events.append(SimulatorEventCreate(machine_id=machine.code, event_type="machine_power_on", timestamp=now)) + elif payload.scenario == "power_off": + events.append(SimulatorEventCreate(machine_id=machine.code, event_type="machine_power_off", timestamp=now)) + elif payload.scenario == "cycle_completed": + events.append( + SimulatorEventCreate( + machine_id=machine.code, + event_type="cycle_completed", + timestamp=now, + cycle_time_sec=cycle_time_sec, + ) + ) + elif payload.scenario == "cycle_burst": + first_event_time = now - timedelta(seconds=cycle_time_sec * max(payload.count - 1, 0)) + for index in range(payload.count): + events.append( + SimulatorEventCreate( + machine_id=machine.code, + event_type="cycle_completed", + timestamp=first_event_time + timedelta(seconds=cycle_time_sec * index), + cycle_time_sec=cycle_time_sec, + ) + ) + elif payload.scenario == "cycle_signal_edge": + current_value = machine.cycle_signal_edge != "falling" + previous_value = not current_value + events.extend( + [ + SimulatorEventCreate( + machine_id=machine.code, + event_type="digital_input_changed", + timestamp=now - timedelta(milliseconds=max(machine.debounce_ms + 100, 500)), + input_name="cycle_signal", + input_value=previous_value, + ), + SimulatorEventCreate( + machine_id=machine.code, + event_type="digital_input_changed", + timestamp=now, + input_name="cycle_signal", + input_value=current_value, + cycle_time_sec=cycle_time_sec, + ), + ] + ) + elif payload.scenario == "debounce_noise": + current_value = machine.cycle_signal_edge != "falling" + previous_value = not current_value + events.extend( + [ + SimulatorEventCreate( + machine_id=machine.code, + event_type="digital_input_changed", + timestamp=now - timedelta(milliseconds=max(machine.debounce_ms - 50, 1)), + input_name="cycle_signal", + input_value=previous_value, + ), + SimulatorEventCreate( + machine_id=machine.code, + event_type="digital_input_changed", + timestamp=now, + input_name="cycle_signal", + input_value=current_value, + cycle_time_sec=cycle_time_sec, + ), + ] + ) + elif payload.scenario == "machine_stopped": + events.append(SimulatorEventCreate(machine_id=machine.code, event_type="machine_stopped", timestamp=now)) + elif payload.scenario == "recover_with_cycle": + events.append( + SimulatorEventCreate( + machine_id=machine.code, + event_type="cycle_completed", + timestamp=now, + cycle_time_sec=cycle_time_sec, + ) + ) + elif payload.scenario == "alarm_on": + events.append( + SimulatorEventCreate( + machine_id=machine.code, + event_type="digital_input_changed", + timestamp=now, + input_name="general_alarm", + input_value=True, + ) + ) + elif payload.scenario == "alarm_off": + events.append( + SimulatorEventCreate( + machine_id=machine.code, + event_type="digital_input_changed", + timestamp=now, + input_name="general_alarm", + input_value=False, + ) + ) + elif payload.scenario == "raw_input": + if not payload.input_name: + raise HTTPException(status_code=400, detail="input_name is required for raw_input") + events.append( + SimulatorEventCreate( + machine_id=machine.code, + event_type="digital_input_changed", + timestamp=now, + input_name=payload.input_name, + input_value=payload.input_value, + ) + ) + else: + raise HTTPException(status_code=400, detail="Unsupported simulator scenario") + + results = [_emit_simulator_event(db, event) for event in events] + return {"scenario": payload.scenario, "machine_id": machine.code, "events": results} + + @app.get("/api/machines/{machine_id}/cycles") def get_machine_cycles(machine_id: int, limit: int = Query(default=100, le=500), db: Session = Depends(get_db)) -> list[dict]: rows = db.execute( diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 2e48583..bf0a667 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -57,3 +57,21 @@ class MachineConfigUpdate(BaseModel): min_cycle_time_sec: int | None = Field(default=None, ge=1) max_cycle_time_sec: int | None = Field(default=None, ge=1) stop_detection_delay_sec: int | None = Field(default=None, ge=1) + + +class SimulatorEventCreate(BaseModel): + machine_id: str + event_type: str + timestamp: datetime | None = None + input_name: str | None = None + input_value: bool | int | str | None = None + cycle_time_sec: float | None = Field(default=None, gt=0) + + +class SimulatorScenarioCreate(BaseModel): + machine_id: str + scenario: str + cycle_time_sec: float | None = Field(default=None, gt=0) + count: int = Field(default=1, ge=1, le=100) + input_name: str | None = None + input_value: bool | int | str | None = None diff --git a/backend/tests/test_events.py b/backend/tests/test_events.py index c3914c5..0b9120b 100644 --- a/backend/tests/test_events.py +++ b/backend/tests/test_events.py @@ -6,9 +6,9 @@ from sqlalchemy import create_engine, select from sqlalchemy.orm import Session, sessionmaker from app.database import Base -from app.main import start_production_order +from app.main import run_simulator_scenario, start_production_order from app.models import Cycle, Downtime, Machine, MachineEvent, ProductionOrder -from app.schemas import MachineEventIn +from app.schemas import MachineEventIn, SimulatorScenarioCreate from app.services.events import _is_matching_edge, detect_missing_cycles, handle_machine_event, normalize_signal_value, parse_machine_event @@ -157,3 +157,41 @@ def test_start_order_rejects_second_running_order_on_machine(db_session: Session assert exc.value.status_code == 400 assert exc.value.detail == "Another order is already running on this machine" + + +def test_simulator_cycle_burst_uses_production_event_logic(db_session: Session) -> None: + machine = seed_running_machine(db_session) + + result = run_simulator_scenario( + SimulatorScenarioCreate( + machine_id=machine.code, + scenario="cycle_burst", + cycle_time_sec=12, + count=3, + ), + db_session, + ) + + cycles = db_session.execute(select(Cycle).where(Cycle.machine_id == machine.id)).scalars().all() + assert result["scenario"] == "cycle_burst" + assert len(result["events"]) == 3 + assert len(cycles) == 3 + assert {cycle.cycle_time_sec for cycle in cycles} == {12} + + +def test_simulator_debounce_noise_is_ignored(db_session: Session) -> None: + machine = seed_running_machine(db_session) + + result = run_simulator_scenario( + SimulatorScenarioCreate( + machine_id=machine.code, + scenario="debounce_noise", + cycle_time_sec=12, + ), + db_session, + ) + + cycles = db_session.execute(select(Cycle).where(Cycle.machine_id == machine.id)).scalars().all() + assert result["scenario"] == "debounce_noise" + assert len(result["events"]) == 2 + assert cycles == [] diff --git a/docker-compose.yml b/docker-compose.yml index 8685749..90e45f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: MQTT_PORT: 1883 MQTT_TOPIC: plasttrack/machines/+/events FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:-http://localhost:5173} - FRONTEND_ORIGINS: ${FRONTEND_ORIGINS:-http://localhost:5173,http://127.0.0.1:5173} + FRONTEND_ORIGINS: ${FRONTEND_ORIGINS:-http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174} depends_on: database: condition: service_healthy @@ -72,5 +72,16 @@ services: ports: - "5173:5173" + simulator-ui: + build: + context: ./simulator-ui + environment: + VITE_API_BASE_URL: ${SIMULATOR_API_BASE_URL:-http://localhost:8000} + VITE_ALLOWED_HOSTS: ${SIMULATOR_ALLOWED_HOSTS:-localhost,127.0.0.1,simulator.plasttrack.sable.ynsdev.site} + depends_on: + - backend + ports: + - "${SIMULATOR_PORT:-5174}:5174" + volumes: postgres_data: diff --git a/simulator-ui/Dockerfile b/simulator-ui/Dockerfile new file mode 100644 index 0000000..84e86ac --- /dev/null +++ b/simulator-ui/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +EXPOSE 5174 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/simulator-ui/index.html b/simulator-ui/index.html new file mode 100644 index 0000000..b1c22c3 --- /dev/null +++ b/simulator-ui/index.html @@ -0,0 +1,12 @@ + + + + + + Plast Track Simulator + + +
+ + + diff --git a/simulator-ui/package-lock.json b/simulator-ui/package-lock.json new file mode 100644 index 0000000..b717b24 --- /dev/null +++ b/simulator-ui/package-lock.json @@ -0,0 +1,1241 @@ +{ + "name": "plast-track-simulator-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "plast-track-simulator-ui", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react-swc": "^4.3.1", + "typescript": "^6.0.3", + "vite": "^8.0.16" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.41.tgz", + "integrity": "sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.41", + "@swc/core-darwin-x64": "1.15.41", + "@swc/core-linux-arm-gnueabihf": "1.15.41", + "@swc/core-linux-arm64-gnu": "1.15.41", + "@swc/core-linux-arm64-musl": "1.15.41", + "@swc/core-linux-ppc64-gnu": "1.15.41", + "@swc/core-linux-s390x-gnu": "1.15.41", + "@swc/core-linux-x64-gnu": "1.15.41", + "@swc/core-linux-x64-musl": "1.15.41", + "@swc/core-win32-arm64-msvc": "1.15.41", + "@swc/core-win32-ia32-msvc": "1.15.41", + "@swc/core-win32-x64-msvc": "1.15.41" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.41.tgz", + "integrity": "sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.41.tgz", + "integrity": "sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.41.tgz", + "integrity": "sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.41.tgz", + "integrity": "sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.41.tgz", + "integrity": "sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.41.tgz", + "integrity": "sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.41.tgz", + "integrity": "sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.41.tgz", + "integrity": "sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.41.tgz", + "integrity": "sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.41.tgz", + "integrity": "sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.41.tgz", + "integrity": "sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.41.tgz", + "integrity": "sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.1.tgz", + "integrity": "sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0", + "@swc/core": "^1.15.11" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/simulator-ui/package.json b/simulator-ui/package.json new file mode 100644 index 0000000..f878093 --- /dev/null +++ b/simulator-ui/package.json @@ -0,0 +1,22 @@ +{ + "name": "plast-track-simulator-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react-swc": "^4.3.1", + "typescript": "^6.0.3", + "vite": "^8.0.16" + } +} diff --git a/simulator-ui/src/main.tsx b/simulator-ui/src/main.tsx new file mode 100644 index 0000000..abd961e --- /dev/null +++ b/simulator-ui/src/main.tsx @@ -0,0 +1,353 @@ +import React, { FormEvent, useEffect, useState } from "react"; +import { createRoot } from "react-dom/client"; +import "./styles.css"; + +type Machine = { + id: number; + code: string; + name: string; + status: string; + powered_on: boolean; +}; + +type ScenarioKey = + | "power_on" + | "power_off" + | "cycle_completed" + | "cycle_burst" + | "cycle_signal_edge" + | "debounce_noise" + | "machine_stopped" + | "recover_with_cycle" + | "alarm_on" + | "alarm_off"; + +type LogEntry = { + id: number; + label: string; + status: "ok" | "error"; + details: string; + createdAt: string; +}; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || window.location.origin; + +const scenarios: Array<{ key: ScenarioKey; title: string; description: string }> = [ + { + key: "power_on", + title: "Power ON", + description: "Simulate machine power available.", + }, + { + key: "power_off", + title: "Power OFF", + description: "Close open downtime and move out of planning.", + }, + { + key: "cycle_completed", + title: "Single Cycle", + description: "Publish one normalized completed cycle.", + }, + { + key: "cycle_burst", + title: "Cycle Burst", + description: "Generate many cycles with the selected cycle time.", + }, + { + key: "cycle_signal_edge", + title: "Passive Edge", + description: "Toggle cycle_signal using the machine configured edge.", + }, + { + key: "debounce_noise", + title: "Debounce Noise", + description: "Send a too-fast edge that should be ignored.", + }, + { + key: "machine_stopped", + title: "Open Stop", + description: "Force an unqualified downtime if an OF is running.", + }, + { + key: "recover_with_cycle", + title: "Recover", + description: "Close downtime by producing a new cycle.", + }, + { + key: "alarm_on", + title: "Alarm ON", + description: "Raise the general_alarm passive signal.", + }, + { + key: "alarm_off", + title: "Alarm OFF", + description: "Clear the general_alarm passive signal.", + }, +]; + +async function apiRequest(path: string, init?: RequestInit): Promise { + const response = await fetch(`${API_BASE_URL}${path}`, { + headers: { + "Content-Type": "application/json", + ...(init?.headers ?? {}), + }, + ...init, + }); + if (!response.ok) { + const body = await response.text(); + throw new Error(body || `Request failed with ${response.status}`); + } + return response.json() as Promise; +} + +function parseInputValue(value: string): boolean | number | string { + const normalized = value.trim().toLowerCase(); + if (["true", "on", "high", "1"].includes(normalized)) { + return true; + } + if (["false", "off", "low", "0"].includes(normalized)) { + return false; + } + const numeric = Number(value); + return Number.isFinite(numeric) && value.trim() !== "" ? numeric : value; +} + +function App() { + const [machines, setMachines] = useState([]); + const [selectedMachine, setSelectedMachine] = useState(""); + const [cycleTimeSec, setCycleTimeSec] = useState(20); + const [burstCount, setBurstCount] = useState(5); + const [rawInputName, setRawInputName] = useState("cycle_signal"); + const [rawInputValue, setRawInputValue] = useState("true"); + const [loading, setLoading] = useState(false); + const [logs, setLogs] = useState([]); + + useEffect(() => { + apiRequest("/api/machines") + .then((items) => { + setMachines(items); + setSelectedMachine((current) => current || items[0]?.code || ""); + }) + .catch((error: Error) => addLog("Load machines", "error", error.message)); + }, []); + + function addLog(label: string, status: LogEntry["status"], details: string) { + setLogs((current) => [ + { + id: Date.now(), + label, + status, + details, + createdAt: new Date().toLocaleTimeString(), + }, + ...current.slice(0, 24), + ]); + } + + async function runScenario(scenario: ScenarioKey) { + if (!selectedMachine) { + addLog("Scenario blocked", "error", "Select a machine first."); + return; + } + setLoading(true); + try { + const result = await apiRequest("/api/simulator/scenarios", { + method: "POST", + body: JSON.stringify({ + machine_id: selectedMachine, + scenario, + cycle_time_sec: cycleTimeSec, + count: burstCount, + }), + }); + addLog(scenario, "ok", JSON.stringify(result, null, 2)); + } catch (error) { + addLog(scenario, "error", error instanceof Error ? error.message : "Unknown error"); + } finally { + setLoading(false); + } + } + + async function sendRawInput(event: FormEvent) { + event.preventDefault(); + if (!selectedMachine || !rawInputName.trim()) { + addLog("Raw input blocked", "error", "Machine and input name are required."); + return; + } + setLoading(true); + try { + const result = await apiRequest("/api/simulator/scenarios", { + method: "POST", + body: JSON.stringify({ + machine_id: selectedMachine, + scenario: "raw_input", + input_name: rawInputName.trim(), + input_value: parseInputValue(rawInputValue), + }), + }); + addLog(`raw:${rawInputName}`, "ok", JSON.stringify(result, null, 2)); + } catch (error) { + addLog("Raw input", "error", error instanceof Error ? error.message : "Unknown error"); + } finally { + setLoading(false); + } + } + + const selectedMachineInfo = machines.find((machine) => machine.code === selectedMachine); + + return ( +
+
+
+

Separate test console

+

Passive Signal Simulator

+

+ Simulate cycles, power states, stops, alarms, signal edges, debounce noise, and raw digital inputs without + adding test controls to the production dashboard. +

+
+
+ Backend API + {API_BASE_URL} +
+
+ +
+
+
+
+

Target

+

Machine

+
+ +
+ + + + {selectedMachineInfo ? ( +
+ +
+ {selectedMachineInfo.status} + {selectedMachineInfo.powered_on ? "Powered ON" : "Powered OFF"} +
+
+ ) : ( +

No machine returned by the backend.

+ )} + +
+ + +
+
+ +
+
+
+

Cases

+

Scenario Launcher

+
+ {loading ? Sending... : null} +
+ +
+ {scenarios.map((scenario) => ( + + ))} +
+
+ +
+
+
+

Manual

+

Raw Passive Input

+
+
+
+ + + +
+
+ +
+
+
+

Audit

+

Simulation Log

+
+ +
+
+ {logs.length === 0 ?

No simulator event sent yet.

: null} + {logs.map((log) => ( +
+ + {log.label} + {log.createdAt} + +
{log.details}
+
+ ))} +
+
+
+
+ ); +} + +createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/simulator-ui/src/styles.css b/simulator-ui/src/styles.css new file mode 100644 index 0000000..50d61ec --- /dev/null +++ b/simulator-ui/src/styles.css @@ -0,0 +1,370 @@ +:root { + color: #2b2b2b; + background: #f5f7fa; + font-family: "Aptos", "Segoe UI", sans-serif; + --primary: #0a2f5a; + --secondary: #2b2b2b; + --accent: #f28c28; + --surface: #ffffff; + --muted: #6f7a86; + --line: #dde5ee; + --success: #0a5a21; + --danger: #b42318; + --shadow: 0 24px 70px rgba(10, 47, 90, 0.12); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.58; +} + +.shell { + min-height: 100vh; + padding: 34px; + background: + radial-gradient(circle at top left, rgba(242, 140, 40, 0.18), transparent 32rem), + linear-gradient(145deg, #f5f7fa 0%, #edf2f7 45%, #ffffff 100%); +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 24px; + align-items: end; + margin: 0 auto 24px; + max-width: 1280px; +} + +.eyebrow { + margin: 0 0 8px; + color: var(--accent); + font-size: 0.74rem; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +h1, +h2 { + margin: 0; + color: var(--primary); + letter-spacing: -0.04em; +} + +h1 { + max-width: 820px; + font-size: clamp(2.4rem, 7vw, 5.8rem); + line-height: 0.9; +} + +h2 { + font-size: 1.45rem; +} + +.hero__copy { + max-width: 760px; + color: var(--muted); + font-size: 1.08rem; + line-height: 1.7; +} + +.connection-card, +.panel { + border: 1px solid rgba(10, 47, 90, 0.1); + border-radius: 28px; + background: rgba(255, 255, 255, 0.86); + box-shadow: var(--shadow); + backdrop-filter: blur(14px); +} + +.connection-card { + min-width: 300px; + padding: 20px; +} + +.connection-card span { + display: block; + color: var(--muted); + font-size: 0.8rem; +} + +.connection-card strong { + display: block; + margin-top: 8px; + color: var(--secondary); + word-break: break-all; +} + +.control-grid { + display: grid; + grid-template-columns: minmax(280px, 0.85fr) minmax(360px, 1.45fr); + gap: 22px; + margin: 0 auto; + max-width: 1280px; +} + +.panel { + padding: 22px; +} + +.panel--scenarios, +.panel--log { + grid-column: span 1; +} + +.panel__header { + display: flex; + gap: 16px; + align-items: center; + justify-content: space-between; + margin-bottom: 18px; +} + +.field { + display: grid; + gap: 8px; + color: var(--secondary); + font-weight: 700; +} + +.field span { + color: var(--muted); + font-size: 0.84rem; + font-weight: 700; +} + +input, +select { + width: 100%; + border: 1px solid var(--line); + border-radius: 16px; + background: #ffffff; + color: var(--secondary); + padding: 13px 14px; + outline: none; +} + +input:focus, +select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 4px rgba(242, 140, 40, 0.15); +} + +.machine-summary { + display: flex; + gap: 12px; + align-items: center; + margin: 18px 0; + border: 1px solid var(--line); + border-radius: 18px; + padding: 16px; + background: #f8fafc; +} + +.machine-summary small { + display: block; + margin-top: 3px; + color: var(--muted); +} + +.status-dot { + width: 18px; + height: 18px; + border-radius: 99px; + background: var(--muted); +} + +.status-dot--production { + background: var(--success); +} + +.status-dot--arret_non_qualifie, +.status-dot--arret_qualifie { + background: var(--danger); +} + +.status-dot--reglage { + background: var(--accent); +} + +.numeric-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.scenario-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.scenario-button { + min-height: 118px; + border: 1px solid rgba(10, 47, 90, 0.12); + border-radius: 22px; + background: linear-gradient(145deg, #ffffff, #f3f7fb); + color: var(--secondary); + padding: 18px; + text-align: left; + transition: + transform 160ms ease, + border-color 160ms ease, + box-shadow 160ms ease; +} + +.scenario-button:hover:not(:disabled) { + transform: translateY(-2px); + border-color: rgba(242, 140, 40, 0.8); + box-shadow: 0 18px 34px rgba(10, 47, 90, 0.12); +} + +.scenario-button strong { + display: block; + color: var(--primary); + font-size: 1rem; +} + +.scenario-button span { + display: block; + margin-top: 8px; + color: var(--muted); + font-size: 0.9rem; + line-height: 1.45; +} + +.raw-form { + display: grid; + gap: 14px; +} + +.primary-button, +.ghost-button { + border-radius: 999px; + padding: 11px 16px; + font-weight: 800; +} + +.primary-button { + border: 0; + background: var(--accent); + color: #241000; +} + +.ghost-button { + border: 1px solid var(--line); + background: #ffffff; + color: var(--primary); +} + +.busy { + border-radius: 99px; + background: rgba(242, 140, 40, 0.12); + color: #9a520f; + padding: 8px 12px; + font-size: 0.86rem; + font-weight: 800; +} + +.log-list { + display: grid; + gap: 10px; + max-height: 520px; + overflow: auto; +} + +.log-entry { + border: 1px solid var(--line); + border-radius: 16px; + background: #ffffff; + overflow: hidden; +} + +.log-entry--ok { + border-left: 5px solid var(--success); +} + +.log-entry--error { + border-left: 5px solid var(--danger); +} + +.log-entry summary { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 13px 14px; + color: var(--primary); + font-weight: 800; + list-style: none; +} + +.log-entry summary::-webkit-details-marker { + display: none; +} + +.log-entry small { + color: var(--muted); + font-weight: 600; +} + +pre { + margin: 0; + border-top: 1px solid var(--line); + padding: 14px; + overflow: auto; + color: #1f2937; + font-size: 0.78rem; + line-height: 1.55; + white-space: pre-wrap; +} + +.empty { + margin: 0; + color: var(--muted); +} + +@media (max-width: 920px) { + .shell { + padding: 20px; + } + + .hero, + .control-grid { + grid-template-columns: 1fr; + } + + .connection-card { + min-width: 0; + } +} + +@media (max-width: 620px) { + .scenario-grid, + .numeric-grid { + grid-template-columns: 1fr; + } + + .panel__header { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/simulator-ui/src/vite-env.d.ts b/simulator-ui/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/simulator-ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/simulator-ui/tsconfig.json b/simulator-ui/tsconfig.json new file mode 100644 index 0000000..fe6cc77 --- /dev/null +++ b/simulator-ui/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true + }, + "include": ["src"] +} diff --git a/simulator-ui/vite.config.ts b/simulator-ui/vite.config.ts new file mode 100644 index 0000000..baba904 --- /dev/null +++ b/simulator-ui/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig, loadEnv } from "vite"; +import react from "@vitejs/plugin-react-swc"; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + const allowedHosts = (env.VITE_ALLOWED_HOSTS ?? "localhost,127.0.0.1") + .split(",") + .map((host) => host.trim()) + .filter(Boolean); + + return { + plugins: [react()], + server: { + host: "0.0.0.0", + port: 5174, + allowedHosts, + }, + }; +});