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}
+
+
+
+
+
+
+
+
window.location.reload()}>
+ Reload
+
+
+
+
+ Machine code
+ setSelectedMachine(event.target.value)}>
+ {machines.map((machine) => (
+
+ {machine.code} - {machine.name}
+
+ ))}
+
+
+
+ {selectedMachineInfo ? (
+
+
+
+ {selectedMachineInfo.status}
+ {selectedMachineInfo.powered_on ? "Powered ON" : "Powered OFF"}
+
+
+ ) : (
+ No machine returned by the backend.
+ )}
+
+
+
+ Cycle time sec
+ setCycleTimeSec(Number(event.target.value))}
+ />
+
+
+ Burst count
+ setBurstCount(Number(event.target.value))}
+ />
+
+
+
+
+
+
+
+
Cases
+
Scenario Launcher
+
+ {loading ?
Sending... : null}
+
+
+
+ {scenarios.map((scenario) => (
+ runScenario(scenario.key)}
+ >
+ {scenario.title}
+ {scenario.description}
+
+ ))}
+
+
+
+
+
+
+
Manual
+
Raw Passive Input
+
+
+
+
+
+
+
+
+
Audit
+
Simulation Log
+
+
setLogs([])}>
+ Clear
+
+
+
+ {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,
+ },
+ };
+});